slashvibe-mcp 0.3.20 → 0.3.21

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 (167) hide show
  1. package/README.md +47 -252
  2. package/analytics.js +107 -0
  3. package/auth-store.js +148 -0
  4. package/auto-update.js +130 -0
  5. package/bridges/bridge-monitor.js +388 -0
  6. package/bridges/discord-bot.js +431 -0
  7. package/bridges/farcaster.js +299 -0
  8. package/bridges/telegram.js +261 -0
  9. package/bridges/webhook-health.js +420 -0
  10. package/bridges/webhook-server.js +437 -0
  11. package/bridges/whatsapp.js +441 -0
  12. package/bridges/x-webhook.js +423 -0
  13. package/config.js +27 -15
  14. package/games/arcade.js +406 -0
  15. package/games/chess.js +451 -0
  16. package/games/colorguess.js +343 -0
  17. package/games/crossword-words.js +171 -0
  18. package/games/crossword.js +461 -0
  19. package/games/drawing.js +347 -0
  20. package/games/gameroulette.js +300 -0
  21. package/games/gamerouter.js +336 -0
  22. package/games/gamestatus.js +337 -0
  23. package/games/guessnumber.js +209 -0
  24. package/games/hangman.js +279 -0
  25. package/games/memory.js +338 -0
  26. package/games/multiplayer-tictactoe.js +389 -0
  27. package/games/pixelart.js +399 -0
  28. package/games/quickduel.js +354 -0
  29. package/games/riddle.js +371 -0
  30. package/games/rockpaperscissors.js +291 -0
  31. package/games/snake.js +406 -0
  32. package/games/storybuilder.js +343 -0
  33. package/games/tictactoe.js +345 -0
  34. package/games/twentyquestions.js +286 -0
  35. package/games/twotruths.js +207 -0
  36. package/games/werewolf.js +508 -0
  37. package/games/wordassociation.js +247 -0
  38. package/games/wordchain.js +135 -0
  39. package/index.js +116 -159
  40. package/intelligence/index.js +9 -2
  41. package/intelligence/interests.js +369 -0
  42. package/notification-emitter.js +77 -0
  43. package/notify.js +5 -1
  44. package/package.json +21 -16
  45. package/prompts.js +1 -1
  46. package/protocol/index.js +73 -0
  47. package/setup.js +480 -0
  48. package/smart-inbox.js +276 -0
  49. package/store/api.js +536 -215
  50. package/store/profiles.js +160 -12
  51. package/tools/_actions.js +362 -21
  52. package/tools/_discovery.js +119 -26
  53. package/tools/_shared/index.js +64 -0
  54. package/tools/_shared.js +234 -0
  55. package/tools/_work-context.js +338 -0
  56. package/tools/_work-context.manual-test.js +199 -0
  57. package/tools/_work-context.test.js +260 -0
  58. package/tools/activity.js +220 -0
  59. package/tools/analytics.js +191 -0
  60. package/tools/approve.js +197 -0
  61. package/tools/artifact-create.js +14 -3
  62. package/tools/artifacts-price.js +107 -0
  63. package/tools/available.js +120 -0
  64. package/tools/broadcast.js +325 -0
  65. package/tools/chat.js +202 -0
  66. package/tools/collaborative-drawing.js +1 -1
  67. package/tools/connection-status.js +178 -0
  68. package/tools/discover.js +350 -34
  69. package/tools/dm.js +80 -8
  70. package/tools/earnings.js +126 -0
  71. package/tools/feed.js +35 -4
  72. package/tools/follow.js +224 -0
  73. package/tools/friends.js +207 -0
  74. package/tools/gig-browse.js +206 -0
  75. package/tools/gig-complete.js +144 -0
  76. package/tools/health.js +87 -0
  77. package/tools/help.js +3 -3
  78. package/tools/idea.js +9 -2
  79. package/tools/inbox.js +289 -105
  80. package/tools/init.js +131 -34
  81. package/tools/invite.js +15 -4
  82. package/tools/leaderboard.js +117 -0
  83. package/tools/lib/git-apply.js +206 -0
  84. package/tools/lib/git-bundle.js +407 -0
  85. package/tools/migrate.js +3 -3
  86. package/tools/multiplayer-game.js +1 -1
  87. package/tools/onboarding.js +7 -7
  88. package/tools/open.js +143 -12
  89. package/tools/party-game.js +1 -1
  90. package/tools/plan.js +225 -0
  91. package/tools/proof-of-work.js +144 -0
  92. package/tools/reply.js +166 -0
  93. package/tools/report.js +1 -1
  94. package/tools/request.js +17 -3
  95. package/tools/schedule.js +367 -0
  96. package/tools/search-messages.js +123 -0
  97. package/tools/session.js +467 -0
  98. package/tools/session_price.js +128 -0
  99. package/tools/settings.js +90 -2
  100. package/tools/ship.js +30 -7
  101. package/tools/smart-check.js +201 -0
  102. package/tools/start.js +147 -12
  103. package/tools/status.js +53 -6
  104. package/tools/streak.js +147 -0
  105. package/tools/stuck.js +297 -0
  106. package/tools/subscribe.js +148 -0
  107. package/tools/subscriptions.js +134 -0
  108. package/tools/suggest-tags.js +6 -8
  109. package/tools/tag-suggestions.js +1 -1
  110. package/tools/tip.js +150 -77
  111. package/tools/token.js +4 -4
  112. package/tools/update.js +1 -1
  113. package/tools/wallet.js +221 -79
  114. package/tools/watch.js +157 -0
  115. package/tools/who.js +30 -1
  116. package/tools/withdraw.js +145 -0
  117. package/tools/work-summary.js +96 -0
  118. package/version.json +10 -8
  119. package/LICENSE +0 -21
  120. package/store/sqlite.js +0 -347
  121. /package/tools/{auto-suggest-connections.js → _deprecated/auto-suggest-connections.js} +0 -0
  122. /package/tools/{away.js → _deprecated/away.js} +0 -0
  123. /package/tools/{back.js → _deprecated/back.js} +0 -0
  124. /package/tools/{bootstrap-skills.js → _deprecated/bootstrap-skills.js} +0 -0
  125. /package/tools/{bridge-dashboard.js → _deprecated/bridge-dashboard.js} +0 -0
  126. /package/tools/{bridge-health.js → _deprecated/bridge-health.js} +0 -0
  127. /package/tools/{bridge-live.js → _deprecated/bridge-live.js} +0 -0
  128. /package/tools/{bridges.js → _deprecated/bridges.js} +0 -0
  129. /package/tools/{colorguess.js → _deprecated/colorguess.js} +0 -0
  130. /package/tools/{discover-insights.js → _deprecated/discover-insights.js} +0 -0
  131. /package/tools/{discover-momentum.js → _deprecated/discover-momentum.js} +0 -0
  132. /package/tools/{discovery-analytics.js → _deprecated/discovery-analytics.js} +0 -0
  133. /package/tools/{discovery-auto-suggest.js → _deprecated/discovery-auto-suggest.js} +0 -0
  134. /package/tools/{discovery-bootstrap.js → _deprecated/discovery-bootstrap.js} +0 -0
  135. /package/tools/{discovery-daily.js → _deprecated/discovery-daily.js} +0 -0
  136. /package/tools/{discovery-dashboard.js → _deprecated/discovery-dashboard.js} +0 -0
  137. /package/tools/{discovery-digest.js → _deprecated/discovery-digest.js} +0 -0
  138. /package/tools/{discovery-hub.js → _deprecated/discovery-hub.js} +0 -0
  139. /package/tools/{discovery-insights.js → _deprecated/discovery-insights.js} +0 -0
  140. /package/tools/{discovery-momentum.js → _deprecated/discovery-momentum.js} +0 -0
  141. /package/tools/{discovery-monitor.js → _deprecated/discovery-monitor.js} +0 -0
  142. /package/tools/{discovery-proactive.js → _deprecated/discovery-proactive.js} +0 -0
  143. /package/tools/{draw.js → _deprecated/draw.js} +0 -0
  144. /package/tools/{farcaster.js → _deprecated/farcaster.js} +0 -0
  145. /package/tools/{forget.js → _deprecated/forget.js} +0 -0
  146. /package/tools/{games-catalog.js → _deprecated/games-catalog.js} +0 -0
  147. /package/tools/{games.js → _deprecated/games.js} +0 -0
  148. /package/tools/{guessnumber.js → _deprecated/guessnumber.js} +0 -0
  149. /package/tools/{hangman.js → _deprecated/hangman.js} +0 -0
  150. /package/tools/{multiplayer-tictactoe.js → _deprecated/multiplayer-tictactoe.js} +0 -0
  151. /package/tools/{mute.js → _deprecated/mute.js} +0 -0
  152. /package/tools/{recall.js → _deprecated/recall.js} +0 -0
  153. /package/tools/{remember.js → _deprecated/remember.js} +0 -0
  154. /package/tools/{riddle.js → _deprecated/riddle.js} +0 -0
  155. /package/tools/{run-bootstrap.js → _deprecated/run-bootstrap.js} +0 -0
  156. /package/tools/{skills-analytics.js → _deprecated/skills-analytics.js} +0 -0
  157. /package/tools/{skills-bootstrap.js → _deprecated/skills-bootstrap.js} +0 -0
  158. /package/tools/{skills-dashboard.js → _deprecated/skills-dashboard.js} +0 -0
  159. /package/tools/{skills-exchange.js → _deprecated/skills-exchange.js} +0 -0
  160. /package/tools/{skills.js → _deprecated/skills.js} +0 -0
  161. /package/tools/{smart-intro.js → _deprecated/smart-intro.js} +0 -0
  162. /package/tools/{storybuilder.js → _deprecated/storybuilder.js} +0 -0
  163. /package/tools/{telegram-bot.js → _deprecated/telegram-bot.js} +0 -0
  164. /package/tools/{telegram-setup.js → _deprecated/telegram-setup.js} +0 -0
  165. /package/tools/{tictactoe.js → _deprecated/tictactoe.js} +0 -0
  166. /package/tools/{twentyquestions.js → _deprecated/twentyquestions.js} +0 -0
  167. /package/tools/{wordassociation.js → _deprecated/wordassociation.js} +0 -0
package/store/api.js CHANGED
@@ -10,14 +10,58 @@ const https = require('https');
10
10
  const http = require('http');
11
11
  const config = require('../config');
12
12
  const crypto = require('../crypto');
13
- const sqlite = require('./sqlite'); // V2 messaging - local persistence
13
+ const authStore = require('../auth-store');
14
14
 
15
15
  const API_URL = process.env.VIBE_API_URL || 'https://www.slashvibe.dev';
16
16
 
17
17
  // Default timeout for API requests (10 seconds)
18
18
  const REQUEST_TIMEOUT = 10000;
19
19
 
20
- function request(method, path, data = null, options = {}) {
20
+ // Retry configuration
21
+ const MAX_RETRIES = 3;
22
+ const INITIAL_RETRY_DELAY = 500; // 500ms
23
+ const MAX_RETRY_DELAY = 5000; // 5 seconds
24
+
25
+ /**
26
+ * Calculate exponential backoff delay with jitter
27
+ * @param {number} attempt - Current attempt number (0-based)
28
+ * @returns {number} Delay in milliseconds
29
+ */
30
+ function getBackoffDelay(attempt) {
31
+ const exponentialDelay = INITIAL_RETRY_DELAY * Math.pow(2, attempt);
32
+ const jitter = Math.random() * 0.3 * exponentialDelay; // 0-30% jitter
33
+ return Math.min(exponentialDelay + jitter, MAX_RETRY_DELAY);
34
+ }
35
+
36
+ /**
37
+ * Sleep for a given duration
38
+ * @param {number} ms - Milliseconds to sleep
39
+ */
40
+ function sleep(ms) {
41
+ return new Promise(resolve => setTimeout(resolve, ms));
42
+ }
43
+
44
+ /**
45
+ * Force reload of config module to pick up auth changes
46
+ * This clears the require cache and reloads the config from disk
47
+ */
48
+ function reloadConfig() {
49
+ try {
50
+ // Clear the config module from require cache
51
+ const configPath = require.resolve('../config');
52
+ delete require.cache[configPath];
53
+ // Re-require to get fresh module
54
+ return require('../config');
55
+ } catch (e) {
56
+ console.error('[api] Failed to reload config:', e.message);
57
+ return config;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Internal single request (no retries)
63
+ */
64
+ function requestOnce(method, path, data = null, options = {}) {
21
65
  return new Promise((resolve, reject) => {
22
66
  const url = new URL(path, API_URL);
23
67
  const isHttps = url.protocol === 'https:';
@@ -29,8 +73,9 @@ function request(method, path, data = null, options = {}) {
29
73
  'User-Agent': 'vibe-mcp/1.0'
30
74
  };
31
75
 
32
- // Add auth token if provided or if we have one stored
33
- const token = options.token || config.getAuthToken();
76
+ // Add auth token: priority is explicit option > in-memory store > config file
77
+ // authStore is the SOURCE OF TRUTH during runtime (immediate updates from OAuth)
78
+ const token = options.token || authStore.getToken() || config.getAuthToken();
34
79
  if (token && options.auth !== false) {
35
80
  headers['Authorization'] = `Bearer ${token}`;
36
81
  }
@@ -47,9 +92,45 @@ function request(method, path, data = null, options = {}) {
47
92
  const req = client.request(reqOptions, (res) => {
48
93
  let body = '';
49
94
  res.on('data', chunk => body += chunk);
50
- res.on('end', () => {
95
+ res.on('end', async () => {
51
96
  // Handle non-2xx responses
52
97
  if (res.statusCode >= 400) {
98
+ // 401 REFRESH: If unauthorized and haven't retried, try to recover
99
+ if (res.statusCode === 401 && !options._retried && options.auth !== false) {
100
+ console.error('[api] 401 received, attempting token refresh...');
101
+
102
+ // First, reload config from disk (in case token was saved but not pushed to store)
103
+ const freshConfig = reloadConfig();
104
+ const diskToken = freshConfig.getAuthToken();
105
+
106
+ // If disk has a different token, sync it to the auth store
107
+ if (diskToken && diskToken !== authStore.getToken()) {
108
+ console.error('[api] Found newer token on disk, syncing to auth store...');
109
+ authStore.setToken(diskToken);
110
+ }
111
+
112
+ // Now check if we have a fresh token to retry with
113
+ const freshToken = authStore.getToken() || diskToken;
114
+
115
+ // Only retry if we got a different/new token
116
+ if (freshToken && freshToken !== token) {
117
+ console.error('[api] Found fresh token, retrying request...');
118
+ try {
119
+ const retryResult = await request(method, path, data, {
120
+ ...options,
121
+ token: freshToken,
122
+ _retried: true
123
+ });
124
+ resolve(retryResult);
125
+ return;
126
+ } catch (retryError) {
127
+ console.error('[api] Retry failed:', retryError.message);
128
+ }
129
+ } else {
130
+ console.error('[api] No fresh token found, not retrying');
131
+ }
132
+ }
133
+
53
134
  try {
54
135
  const parsed = JSON.parse(body);
55
136
  resolve({ success: false, error: parsed.error || `HTTP ${res.statusCode}`, statusCode: res.statusCode });
@@ -70,11 +151,11 @@ function request(method, path, data = null, options = {}) {
70
151
  // Handle timeout
71
152
  req.on('timeout', () => {
72
153
  req.destroy();
73
- resolve({ success: false, error: 'Request timeout', timeout: true });
154
+ resolve({ success: false, error: 'Request timeout', timeout: true, retryable: true });
74
155
  });
75
156
 
76
157
  req.on('error', (e) => {
77
- resolve({ success: false, error: e.message, network: true });
158
+ resolve({ success: false, error: e.message, network: true, retryable: true });
78
159
  });
79
160
 
80
161
  if (data) {
@@ -84,8 +165,45 @@ function request(method, path, data = null, options = {}) {
84
165
  });
85
166
  }
86
167
 
168
+ /**
169
+ * Make HTTP request with exponential backoff retry for transient failures
170
+ */
171
+ async function request(method, path, data = null, options = {}) {
172
+ const maxRetries = options.maxRetries ?? MAX_RETRIES;
173
+ const skipRetry = options._skipRetry || false;
174
+
175
+ // Don't retry if explicitly disabled or already in a retry chain
176
+ if (skipRetry || maxRetries === 0) {
177
+ return requestOnce(method, path, data, options);
178
+ }
179
+
180
+ let lastResult;
181
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
182
+ lastResult = await requestOnce(method, path, data, { ...options, _skipRetry: true });
183
+
184
+ // Success or non-retryable error - return immediately
185
+ if (!lastResult.retryable) {
186
+ return lastResult;
187
+ }
188
+
189
+ // Don't retry on last attempt
190
+ if (attempt < maxRetries) {
191
+ const delay = getBackoffDelay(attempt);
192
+ console.error(`[api] Retrying ${method} ${path} in ${Math.round(delay)}ms (attempt ${attempt + 1}/${maxRetries})`);
193
+ await sleep(delay);
194
+ }
195
+ }
196
+
197
+ // All retries exhausted
198
+ console.error(`[api] All ${maxRetries} retries failed for ${method} ${path}`);
199
+ return lastResult;
200
+ }
201
+
87
202
  // ============ PRESENCE ============
88
203
 
204
+ // Use v2 API by default (Postgres-backed)
205
+ const USE_V2_PRESENCE = process.env.VIBE_PRESENCE_V1 !== 'true';
206
+
89
207
  // Session ID for this MCP instance
90
208
  let currentSessionId = null;
91
209
 
@@ -116,7 +234,7 @@ async function registerSession(sessionId, handle, building = null, publicKey = n
116
234
  currentSessionId = result.sessionId;
117
235
 
118
236
  // Save token for future authenticated requests (persist to shared config)
119
- config.savePrivyToken(result.token);
237
+ config.saveAuthToken(result.token);
120
238
 
121
239
  console.error(`[vibe] Registered @${handle} with session ${result.sessionId}`);
122
240
  } else if (result.success) {
@@ -125,7 +243,7 @@ async function registerSession(sessionId, handle, building = null, publicKey = n
125
243
  console.error(`[vibe] Registered @${handle} (legacy mode)`);
126
244
  }
127
245
 
128
- // Also register user in users DB (for @vibe welcome tracking)
246
+ // Also register user in users DB (for @seth welcome tracking)
129
247
  // AIRC: Include public key for identity
130
248
  try {
131
249
  const userData = {
@@ -149,8 +267,9 @@ async function registerSession(sessionId, handle, building = null, publicKey = n
149
267
 
150
268
  async function heartbeat(handle, one_liner, context = null) {
151
269
  try {
152
- // Token-based auth: server extracts handle from token
153
- // Only need to send workingOn and context
270
+ const endpoint = USE_V2_PRESENCE ? '/api/v2/presence' : '/api/presence';
271
+
272
+ // Build payload
154
273
  const payload = { workingOn: one_liner };
155
274
 
156
275
  // Fallback: if no token, send username (legacy support)
@@ -158,12 +277,23 @@ async function heartbeat(handle, one_liner, context = null) {
158
277
  payload.username = handle;
159
278
  }
160
279
 
161
- // Add context (mood, file, etc.) if provided
280
+ // Add context fields (v2 flattens these)
162
281
  if (context) {
163
- payload.context = context;
282
+ if (USE_V2_PRESENCE) {
283
+ // v2: flat structure
284
+ if (context.mood) payload.mood = context.mood;
285
+ if (context.file) payload.file = context.file;
286
+ if (context.project) payload.project = context.project;
287
+ if (context.awayMessage) payload.awayMessage = context.awayMessage;
288
+ if (context.sessionId) payload.sessionId = context.sessionId;
289
+ if (context.availableFor !== undefined) payload.availableFor = context.availableFor;
290
+ } else {
291
+ // v1: nested context
292
+ payload.context = context;
293
+ }
164
294
  }
165
295
 
166
- await request('POST', '/api/presence', payload);
296
+ await request('POST', endpoint, payload);
167
297
  } catch (e) {
168
298
  console.error('Heartbeat failed:', e.message);
169
299
  }
@@ -171,24 +301,21 @@ async function heartbeat(handle, one_liner, context = null) {
171
301
 
172
302
  async function sendTypingIndicator(handle, toHandle) {
173
303
  try {
174
- // Token auth: server extracts sender from token
175
- const payload = { typingTo: toHandle };
176
-
177
- // Fallback for legacy
178
- if (!config.getAuthToken()) {
179
- payload.username = handle;
180
- }
181
-
182
- await request('POST', '/api/presence', payload);
304
+ // Use dedicated typing API with 5s TTL
305
+ await request('POST', '/api/typing', {
306
+ from: handle,
307
+ to: toHandle,
308
+ });
183
309
  } catch (e) {
184
- console.error('Typing indicator failed:', e.message);
310
+ // Silent fail - typing is best-effort
185
311
  }
186
312
  }
187
313
 
188
314
  async function getTypingUsers(forHandle) {
189
315
  try {
190
- const result = await request('GET', `/api/presence?user=${forHandle}&typing=true`);
191
- return result.typingUsers || [];
316
+ // Use dedicated typing API to get all users typing to forHandle
317
+ const result = await request('GET', `/api/typing?to=${forHandle}`);
318
+ return result.typing || [];
192
319
  } catch (e) {
193
320
  return [];
194
321
  }
@@ -196,30 +323,55 @@ async function getTypingUsers(forHandle) {
196
323
 
197
324
  async function getActiveUsers() {
198
325
  try {
199
- const result = await request('GET', '/api/presence');
326
+ const endpoint = USE_V2_PRESENCE ? '/api/v2/presence' : '/api/presence';
327
+ const result = await request('GET', endpoint);
328
+
200
329
  // Combine active and away users
201
330
  const users = [...(result.active || []), ...(result.away || [])];
202
- return users.map(u => ({
203
- handle: u.username,
331
+
332
+ // Map to normalized format (v2 uses 'handle', v1 uses 'username')
333
+ const mappedUsers = users.map(u => ({
334
+ handle: u.handle || u.username,
204
335
  one_liner: u.workingOn,
205
336
  lastSeen: new Date(u.lastSeen).getTime(),
206
337
  firstSeen: u.firstSeen ? new Date(u.firstSeen).getTime() : null,
207
338
  status: u.status,
208
- // Mood: explicit (context.mood) or inferred (u.mood)
209
- mood: u.context?.mood || u.mood || null,
339
+ // Mood: v2 is flat, v1 is nested
340
+ mood: u.mood || u.context?.mood || null,
210
341
  mood_inferred: u.mood_inferred || false,
211
342
  mood_reason: u.mood_reason || null,
212
343
  builderMode: u.builderMode || null,
213
344
  // Context sharing fields
214
- file: u.context?.file || null,
215
- branch: u.context?.branch || null,
216
- repo: u.context?.repo || null,
217
- error: u.context?.error || null,
218
- note: u.context?.note || null,
345
+ file: u.file || u.context?.file || null,
346
+ branch: u.branch || u.context?.branch || null,
347
+ repo: u.repo || u.context?.repo || null,
348
+ error: u.error || u.context?.error || null,
349
+ note: u.note || u.context?.note || null,
219
350
  // Away status
220
- awayMessage: u.context?.awayMessage || null,
221
- awayAt: u.context?.awayAt || null
351
+ awayMessage: u.awayMessage || u.context?.awayMessage || null,
352
+ awayAt: u.awayAt || u.context?.awayAt || null,
353
+ // v2 additions
354
+ isLive: u.isLive || false,
355
+ isAgent: u.isAgent || false
222
356
  }));
357
+
358
+ // Sync presence data to local profiles (non-blocking)
359
+ // This enables discovery to find users by what they're building
360
+ try {
361
+ const profiles = require('./profiles');
362
+ profiles.syncFromPresence(mappedUsers).then(synced => {
363
+ if (synced > 0) {
364
+ // Auto-infer interests for new/updated profiles
365
+ profiles.inferMissingInterests().catch(e =>
366
+ console.error('[presence] interest inference failed:', e.message)
367
+ );
368
+ }
369
+ }).catch(e => console.error('[presence] sync failed:', e.message));
370
+ } catch (e) {
371
+ // Non-fatal: profiles module may not be available in some contexts
372
+ }
373
+
374
+ return mappedUsers;
223
375
  } catch (e) {
224
376
  console.error('Who failed:', e.message);
225
377
  return [];
@@ -227,42 +379,56 @@ async function getActiveUsers() {
227
379
  }
228
380
 
229
381
  async function setVisibility(handle, visible) {
230
- // TODO: implement visibility toggle API
382
+ try {
383
+ const endpoint = USE_V2_PRESENCE ? '/api/v2/presence' : '/api/presence';
384
+ const result = await request('POST', endpoint, {
385
+ action: 'visibility',
386
+ visible: visible
387
+ });
388
+
389
+ if (result.success === false) {
390
+ console.error('[setVisibility] API error:', result.error);
391
+ return { success: false, error: result.error };
392
+ }
393
+
394
+ return { success: true, visible };
395
+ } catch (e) {
396
+ console.error('[setVisibility] Error:', e.message);
397
+ return { success: false, error: e.message };
398
+ }
231
399
  }
232
400
 
233
401
  // ============ MESSAGES ============
234
402
 
235
- async function sendMessage(from, to, body, type = 'dm', payload = null) {
236
- // V2 MESSAGING: Save to SQLite first (optimistic UI)
237
- const local_id = require('crypto').randomUUID();
238
- const created_at = new Date().toISOString();
239
-
240
- try {
241
- // 1. Save to local SQLite (optimistic - before API call)
242
- sqlite.saveLocalMessage({
243
- local_id,
244
- from_handle: from,
245
- to_handle: to,
246
- content: body || '',
247
- created_at,
248
- status: 'pending'
249
- });
250
- } catch (sqliteError) {
251
- // Don't fail message send if SQLite fails (just log)
252
- console.warn('[SQLite] Failed to save message locally:', sqliteError.message);
253
- }
403
+ // Use v2 API by default (Postgres-backed, cross-client sync)
404
+ const USE_V2_MESSAGES = process.env.VIBE_MESSAGES_V1 !== 'true';
254
405
 
406
+ async function sendMessage(from, to, body, type = 'dm', payload = null, options = {}) {
255
407
  try {
256
408
  let data;
257
-
258
- // Check if using Privy auth (server-side signing)
259
- if (config.hasPrivyAuth()) {
260
- // NEW: Privy auth flow - server handles signing
409
+ let endpoint = '/api/messages';
410
+
411
+ // V2 API: Uses Postgres, simpler payload
412
+ const hasAuth = config.hasOAuth();
413
+ console.error('[vibe] sendMessage check: USE_V2_MESSAGES=' + USE_V2_MESSAGES + ', hasOAuth=' + hasAuth);
414
+ if (USE_V2_MESSAGES && hasAuth) {
415
+ endpoint = '/api/v2/messages';
416
+ data = {
417
+ to,
418
+ body: body || '',
419
+ payload: payload || undefined,
420
+ reply_to: options.replyTo || undefined, // Threaded reply support
421
+ };
422
+ console.error('[vibe] Sending message via v2 API (Postgres-backed) to:', to, 'body length:', (body || '').length);
423
+ }
424
+ // V1 with OAuth (server-side signing)
425
+ else if (config.hasOAuth()) {
426
+ // OAuth flow - server handles signing
261
427
  // Just send message data, server signs it
262
428
  data = { to, body: body || undefined, text: body };
263
429
  if (payload) data.payload = payload;
264
430
 
265
- console.error('[vibe] Sending message via Privy auth (server-side signing)');
431
+ console.error('[vibe] Sending message via OAuth (server-side signing)');
266
432
  } else {
267
433
  // LEGACY: Create signed message if we have a keypair
268
434
  const keypair = config.getKeypair();
@@ -287,53 +453,34 @@ async function sendMessage(from, to, body, type = 'dm', payload = null) {
287
453
  }
288
454
  }
289
455
 
290
- const result = await request('POST', '/api/messages', data);
456
+ console.error('[vibe] Sending request to endpoint:', endpoint, 'with data:', JSON.stringify(data).substring(0, 200));
457
+ const result = await request('POST', endpoint, data);
458
+ console.error('[vibe] Got result:', JSON.stringify(result).substring(0, 300));
291
459
 
292
460
  // Handle auth errors
293
461
  if (!result.success && result.error?.includes('Authentication')) {
294
- // Mark as failed in SQLite
295
- try { sqlite.updateMessageStatus(local_id, 'failed'); } catch (e) {}
296
462
  console.error('[vibe] Auth failed for message. Try `vibe init` to re-register.');
297
463
  return { error: 'auth_failed', message: 'Authentication failed. Try `vibe init` to re-register.' };
298
464
  }
299
465
 
300
466
  // Handle expired token
301
467
  if (result.statusCode === 401) {
302
- // Mark as failed in SQLite
303
- try { sqlite.updateMessageStatus(local_id, 'failed'); } catch (e) {}
304
468
  console.error('[vibe] Auth expired. Run browser auth to refresh token.');
305
469
  return { error: 'auth_expired', message: 'Auth expired. Run `vibe init` to refresh token.' };
306
470
  }
307
471
 
308
472
  // Handle storage errors (KV write failed)
309
473
  if (!result.success && result.error === 'storage_error') {
310
- // Mark as failed in SQLite
311
- try { sqlite.updateMessageStatus(local_id, 'failed'); } catch (e) {}
312
474
  console.error('[vibe] Storage error:', result.details || result.message);
313
475
  return { error: 'storage_error', message: result.message || 'Failed to save message. Please try again.' };
314
476
  }
315
477
 
316
478
  // Handle other errors
317
479
  if (!result.success && result.error) {
318
- // Mark as failed in SQLite
319
- try { sqlite.updateMessageStatus(local_id, 'failed'); } catch (e) {}
320
480
  console.error('[vibe] Send error:', result.error, result.message);
321
481
  return { error: result.error, message: result.message || 'Failed to send message.' };
322
482
  }
323
483
 
324
- // V2 MESSAGING: Update SQLite with server_id, thread_id and mark as sent
325
- if (result.success || result.message) {
326
- try {
327
- // V2 Postgres: result.message.id, result.message.thread_id
328
- const message = result.message || {};
329
- const server_id = message.id || result.messageId || result.id || null;
330
- const thread_id = message.thread_id || null;
331
- sqlite.updateMessageStatus(local_id, 'sent', server_id, thread_id);
332
- } catch (sqliteError) {
333
- console.warn('[SQLite] Failed to update message status:', sqliteError.message);
334
- }
335
- }
336
-
337
484
  // Emit list_changed notification for successful message send
338
485
  // This allows other Claude Code instances to see the new message instantly
339
486
  if (result.success || result.message) {
@@ -345,92 +492,96 @@ async function sendMessage(from, to, body, type = 'dm', payload = null) {
345
492
  return result.message;
346
493
  } catch (e) {
347
494
  console.error('Send failed:', e.message);
348
- // Mark as failed in SQLite
349
- try { sqlite.updateMessageStatus(local_id, 'failed'); } catch (sqliteErr) {}
350
495
  return null;
351
496
  }
352
497
  }
353
498
 
354
499
  async function getInbox(handle) {
355
500
  try {
356
- // V2 MESSAGING: Hybrid approach - SQLite (fast) + API (sync)
501
+ // V2: Use threads endpoint (Postgres-backed, cross-client sync)
502
+ if (USE_V2_MESSAGES) {
503
+ const result = await request('GET', '/api/v2/threads');
504
+
505
+ if (result.success === false) {
506
+ console.error('[getInbox] V2 API error:', result.error);
507
+ // Fall back to v1
508
+ return getInboxV1(handle);
509
+ }
357
510
 
358
- // 1. Get from local SQLite first
359
- let localInbox = [];
360
- try {
361
- localInbox = sqlite.getInboxThreads(handle);
362
- } catch (sqliteError) {
363
- console.warn('[SQLite] Failed to read inbox:', sqliteError.message);
511
+ // Map v2 response to expected format
512
+ return (result.threads || []).map(t => ({
513
+ handle: t.with,
514
+ thread_id: t.id,
515
+ messages: [], // Not loaded until thread is opened
516
+ unread: t.unread || 0,
517
+ lastMessage: t.last_message?.body,
518
+ lastTimestamp: t.last_message?.created_at ? new Date(t.last_message.created_at).getTime() : null
519
+ }));
364
520
  }
365
521
 
366
- // 2. Fetch from API (sync with backend)
522
+ // V1 fallback
523
+ return getInboxV1(handle);
524
+ } catch (e) {
525
+ console.error('Inbox failed:', e.message);
526
+ return [];
527
+ }
528
+ }
529
+
530
+ // V1 inbox implementation (now handles V2 format since /api/messages returns V2)
531
+ async function getInboxV1(handle) {
532
+ try {
533
+ // /api/messages now returns V2 format: { threads, total_unread }
367
534
  const result = await request('GET', `/api/messages?user=${handle}`);
368
535
 
369
- // V2 Postgres: result.threads[] with thread_id
370
- const threads = result.threads || [];
536
+ // Check for API errors (auth failures, etc.)
537
+ if (result.success === false) {
538
+ console.error('[getInbox] API error:', result.error, result.message);
539
+ return [];
540
+ }
371
541
 
372
- // Merge threads into SQLite for persistence
373
- if (threads.length > 0) {
374
- try {
375
- threads.forEach(thread => {
376
- const msg = thread.last_message;
377
- if (msg) {
378
- sqlite.mergeServerMessages([{
379
- server_id: msg.id,
380
- thread_id: thread.id, // V2 thread_id
381
- from_handle: msg.from,
382
- to_handle: handle === msg.from ? thread.with : handle,
383
- content: msg.body,
384
- created_at: msg.created_at,
385
- status: 'delivered'
386
- }]);
387
- }
388
- });
389
- } catch (sqliteError) {
390
- console.warn('[SQLite] Failed to merge inbox threads:', sqliteError.message);
391
- }
542
+ // V2 format: map threads to expected format
543
+ if (result.threads) {
544
+ return result.threads.map(t => ({
545
+ handle: t.with,
546
+ thread_id: t.id,
547
+ messages: [],
548
+ unread: t.unread || 0,
549
+ lastMessage: t.last_message?.body,
550
+ lastTimestamp: t.last_message?.created_at ? new Date(t.last_message.created_at).getTime() : null
551
+ }));
392
552
  }
393
553
 
394
- // Return V2 format
395
- return threads.map(thread => ({
396
- handle: thread.with,
397
- messages: thread.last_message ? [{
398
- from: thread.last_message.from,
399
- body: thread.last_message.body,
400
- timestamp: new Date(thread.last_message.created_at).getTime(),
401
- read: thread.unread === 0
402
- }] : [],
403
- unread: thread.unread,
404
- lastMessage: thread.last_message?.body,
405
- lastTimestamp: thread.last_message ? new Date(thread.last_message.created_at).getTime() : 0
554
+ // Legacy V1 format (bySender) - kept for backwards compatibility
555
+ const bySender = result.bySender || {};
556
+ return Object.entries(bySender).map(([sender, messages]) => ({
557
+ handle: sender,
558
+ messages: messages.map(m => ({
559
+ from: m.from,
560
+ body: m.text,
561
+ timestamp: new Date(m.createdAt).getTime(),
562
+ read: m.read
563
+ })),
564
+ unread: messages.filter(m => !m.read).length,
565
+ lastMessage: messages[0]?.text,
566
+ lastTimestamp: new Date(messages[0]?.createdAt).getTime()
406
567
  }));
407
568
  } catch (e) {
408
- console.error('Inbox failed:', e.message);
409
-
410
- // Fallback to SQLite if API fails
411
- try {
412
- const localInbox = sqlite.getInboxThreads(handle);
413
- return localInbox.map(thread => ({
414
- handle: thread.partner,
415
- messages: [thread.latestMessage].map(m => ({
416
- from: m.from_handle,
417
- body: m.content,
418
- timestamp: new Date(m.created_at).getTime(),
419
- read: m.status === 'read'
420
- })),
421
- unread: thread.unreadCount,
422
- lastMessage: thread.latestMessage.content,
423
- lastTimestamp: new Date(thread.latestMessage.created_at).getTime()
424
- }));
425
- } catch (sqliteError) {
426
- return [];
427
- }
569
+ console.error('Inbox v1 failed:', e.message);
570
+ return [];
428
571
  }
429
572
  }
430
573
 
431
574
  async function getUnreadCount(handle) {
432
575
  try {
433
- // Use unified messages endpoint - returns { inbox, unread, bySender }
576
+ // V2: Get from threads endpoint
577
+ if (USE_V2_MESSAGES) {
578
+ const result = await request('GET', '/api/v2/threads');
579
+ if (result.success !== false) {
580
+ return result.total_unread || 0;
581
+ }
582
+ }
583
+
584
+ // V1 fallback
434
585
  const result = await request('GET', `/api/messages?user=${handle}`);
435
586
  return result.unread || 0;
436
587
  } catch (e) {
@@ -438,96 +589,198 @@ async function getUnreadCount(handle) {
438
589
  }
439
590
  }
440
591
 
592
+ /**
593
+ * Get count of active live broadcasts
594
+ * Used in presence footer to show "X live now"
595
+ */
596
+ async function getLiveBroadcastCount() {
597
+ try {
598
+ const result = await request('GET', '/api/watch', null, { auth: false });
599
+ if (result.broadcasts) {
600
+ return Object.keys(result.broadcasts).length;
601
+ }
602
+ return 0;
603
+ } catch (e) {
604
+ return 0;
605
+ }
606
+ }
607
+
441
608
  // Get raw inbox messages (for notification checks)
609
+ // V2: Fetches threads and constructs message objects from unread threads
610
+ // Read state is synced via Postgres cursors, so if you read on iOS it's reflected here
442
611
  async function getRawInbox(handle) {
443
612
  try {
444
- // Use unified messages endpoint - returns { inbox, unread, bySender }
613
+ // V2: Get threads with unread counts (read state synced via Postgres)
445
614
  const result = await request('GET', `/api/messages?user=${handle}`);
446
- return result.inbox || [];
615
+
616
+ // V2 format: { threads, total_unread }
617
+ const threads = result.threads || [];
618
+
619
+ // Transform threads with unread messages into message-like objects
620
+ // for compatibility with notification system
621
+ const unreadMessages = [];
622
+
623
+ for (const thread of threads) {
624
+ if (thread.unread > 0 && thread.last_message) {
625
+ // Create message object from thread's last message
626
+ unreadMessages.push({
627
+ id: thread.last_message.id,
628
+ from: thread.last_message.from,
629
+ to: handle,
630
+ text: thread.last_message.body,
631
+ body: thread.last_message.body,
632
+ createdAt: thread.last_message.created_at,
633
+ read: false, // If it's in unread threads, it's unread
634
+ thread_id: thread.id,
635
+ unread_count: thread.unread,
636
+ });
637
+ }
638
+ }
639
+
640
+ return unreadMessages;
447
641
  } catch (e) {
642
+ console.error('[getRawInbox] Error:', e.message);
448
643
  return [];
449
644
  }
450
645
  }
451
646
 
452
647
  async function getThread(myHandle, theirHandle) {
453
648
  try {
454
- // V2 MESSAGING: Hybrid approach - SQLite (fast) + API (sync)
649
+ // V2: Use threads endpoint with thread_id lookup
650
+ if (USE_V2_MESSAGES) {
651
+ // First, get threads to find thread_id
652
+ const threadsResult = await request('GET', '/api/v2/threads');
653
+
654
+ if (threadsResult.success !== false) {
655
+ const thread = (threadsResult.threads || []).find(t =>
656
+ t.with?.toLowerCase() === theirHandle.toLowerCase()
657
+ );
658
+
659
+ if (thread?.id) {
660
+ // Get messages in thread
661
+ const messagesResult = await request('GET', `/api/v2/threads/${thread.id}`);
662
+
663
+ if (messagesResult.success !== false) {
664
+ // Map messages with read receipt info
665
+ const messages = (messagesResult.messages || []).map(m => ({
666
+ from: m.from,
667
+ isAgent: false,
668
+ body: m.body,
669
+ payload: m.payload || null,
670
+ timestamp: new Date(m.created_at).getTime(),
671
+ direction: m.from === myHandle ? 'sent' : 'received',
672
+ // Read receipt: for sent messages, has recipient read it?
673
+ readByThem: m.read_by_them || false,
674
+ }));
675
+
676
+ // Attach their read cursor info to the result
677
+ messages._theirReadCursor = messagesResult.their_read_cursor;
678
+ return messages;
679
+ }
680
+ }
681
+ }
455
682
 
456
- // 1. Get from local SQLite first (instant, works offline)
457
- let localMessages = [];
458
- try {
459
- localMessages = sqlite.getThreadMessages(myHandle, theirHandle);
460
- } catch (sqliteError) {
461
- console.warn('[SQLite] Failed to read thread:', sqliteError.message);
683
+ // Fall back to v1 if v2 fails or thread not found
684
+ console.error('[getThread] V2 failed, falling back to v1');
462
685
  }
463
686
 
464
- // 2. Fetch from API (sync with backend)
687
+ // V1 fallback
465
688
  const result = await request('GET', `/api/messages?user=${myHandle}&with=${theirHandle}`);
466
-
467
- // V2 Postgres: result.messages[] (not result.thread)
468
- const apiMessages = result.messages || result.thread || [];
469
-
470
- // 3. Merge API messages into SQLite (for future reads)
471
- if (apiMessages.length > 0) {
472
- try {
473
- sqlite.mergeServerMessages(apiMessages.map(m => ({
474
- server_id: m.id || m.messageId,
475
- thread_id: m.thread_id || null, // V2 thread_id (if present)
476
- from_handle: m.from,
477
- to_handle: m.to || (m.from === myHandle ? theirHandle : myHandle),
478
- content: m.body || m.text || '', // V2 uses 'body'
479
- created_at: m.created_at || m.createdAt || new Date().toISOString(),
480
- status: 'delivered',
481
- sent_at: m.sent_at || m.sentAt || m.created_at || m.createdAt,
482
- delivered_at: m.delivered_at || m.deliveredAt || m.created_at || m.createdAt
483
- })));
484
- } catch (sqliteError) {
485
- console.warn('[SQLite] Failed to merge messages:', sqliteError.message);
486
- }
487
- }
488
-
489
- // 4. Return merged result (prefer API for latest, fallback to local)
490
- const messages = apiMessages.length > 0 ? apiMessages : localMessages.map(m => ({
491
- id: m.server_id,
492
- from: m.from_handle,
493
- to: m.to_handle,
494
- body: m.content, // V2 uses 'body'
495
- created_at: m.created_at
496
- }));
497
-
498
- return messages.map(m => ({
689
+ return (result.thread || []).map(m => ({
499
690
  from: m.from,
500
- isAgent: m.isAgent || m.is_agent || false,
501
- body: m.body || m.text || m.content || '', // V2: m.body, fallback to legacy
691
+ isAgent: m.isAgent || m.is_agent || false, // Support both naming conventions
692
+ body: m.text,
502
693
  payload: m.payload || null,
503
- timestamp: new Date(m.created_at || m.createdAt).getTime(),
694
+ timestamp: new Date(m.createdAt).getTime(),
504
695
  direction: m.direction
505
696
  }));
506
697
  } catch (e) {
507
698
  console.error('Thread failed:', e.message);
699
+ return [];
700
+ }
701
+ }
508
702
 
509
- // Fallback to SQLite if API fails
703
+ async function markThreadRead(myHandle, theirHandle, lastMessageId = null) {
704
+ // V2: Explicit mark read via PATCH
705
+ if (USE_V2_MESSAGES && lastMessageId) {
510
706
  try {
511
- const localMessages = sqlite.getThreadMessages(myHandle, theirHandle);
512
- return localMessages.map(m => ({
513
- from: m.from_handle,
514
- isAgent: false,
515
- body: m.content,
516
- payload: null,
517
- timestamp: new Date(m.created_at).getTime(),
518
- direction: m.from_handle === myHandle ? 'sent' : 'received'
519
- }));
520
- } catch (sqliteError) {
521
- return [];
707
+ // Find thread ID
708
+ const threadsResult = await request('GET', '/api/v2/threads');
709
+ if (threadsResult.success !== false) {
710
+ const thread = (threadsResult.threads || []).find(t =>
711
+ t.with?.toLowerCase() === theirHandle.toLowerCase()
712
+ );
713
+
714
+ if (thread?.id) {
715
+ await request('PATCH', `/api/v2/threads/${thread.id}/read`, {
716
+ last_read_id: lastMessageId,
717
+ client: 'terminal'
718
+ });
719
+ }
720
+ }
721
+ } catch (e) {
722
+ console.error('[markThreadRead] V2 error:', e.message);
522
723
  }
523
724
  }
524
- }
525
725
 
526
- async function markThreadRead(myHandle, theirHandle) {
527
- // No-op: Backend automatically marks messages as read when getThread() is called
726
+ // V1: No-op - Backend automatically marks messages as read when getThread() is called
528
727
  // See: api/messages.js thread endpoint (GET /api/messages?user=X&with=Y)
529
728
  }
530
729
 
730
+ /**
731
+ * Mark messages as delivered
732
+ * Called when messages are received via SSE or poll
733
+ * @param {string[]} messageIds - Array of message IDs to mark as delivered
734
+ */
735
+ async function markMessagesDelivered(messageIds) {
736
+ if (!messageIds || messageIds.length === 0) return { marked: 0 };
737
+
738
+ try {
739
+ const result = await request('POST', '/api/v2/messages/delivered', {
740
+ message_ids: messageIds
741
+ });
742
+
743
+ return result;
744
+ } catch (e) {
745
+ console.error('[markMessagesDelivered] Error:', e.message);
746
+ return { marked: 0, error: e.message };
747
+ }
748
+ }
749
+
750
+ /**
751
+ * Search messages
752
+ * @param {string} query - Search term
753
+ * @param {object} options - Search options
754
+ * @param {string} [options.with] - Filter to conversation with specific user
755
+ * @param {number} [options.limit] - Max results (default 50)
756
+ */
757
+ async function searchMessages(query, options = {}) {
758
+ try {
759
+ let url = `/api/v2/messages/search?q=${encodeURIComponent(query)}`;
760
+
761
+ if (options.with) {
762
+ url += `&with=${encodeURIComponent(options.with)}`;
763
+ }
764
+ if (options.limit) {
765
+ url += `&limit=${options.limit}`;
766
+ }
767
+ if (options.offset) {
768
+ url += `&offset=${options.offset}`;
769
+ }
770
+
771
+ const result = await request('GET', url);
772
+
773
+ if (result.success === false) {
774
+ throw new Error(result.error || 'Search failed');
775
+ }
776
+
777
+ return result;
778
+ } catch (e) {
779
+ console.error('[searchMessages] Error:', e.message);
780
+ throw e;
781
+ }
782
+ }
783
+
531
784
  // ============ CONSENT ============
532
785
 
533
786
  async function getConsentStatus(from, to) {
@@ -636,11 +889,11 @@ async function checkInviteCode(code) {
636
889
  // ============ AUTH ============
637
890
 
638
891
  /**
639
- * Verify a Privy token with the server
640
- * @param {string} token - Privy JWT token
892
+ * Verify an auth token with the server
893
+ * @param {string} token - JWT token from GitHub OAuth
641
894
  * @returns {Promise<{valid: boolean, handle?: string, error?: string}>}
642
895
  */
643
- async function verifyPrivyToken(token) {
896
+ async function verifyAuthToken(token) {
644
897
  try {
645
898
  const result = await request('POST', '/api/auth/verify', {}, { token, auth: true });
646
899
 
@@ -757,6 +1010,42 @@ async function getChecklistStatus(handle) {
757
1010
  }
758
1011
  }
759
1012
 
1013
+ /**
1014
+ * Track completion of an onboarding task
1015
+ * @param {string} handle - User handle
1016
+ * @param {string} taskId - Task ID (e.g., 'read_welcome', 'reply_seth')
1017
+ * @param {Object} metadata - Optional metadata about the completion
1018
+ * @returns {Promise<{success: boolean, taskId: string, completedAt: string}>}
1019
+ */
1020
+ async function trackChecklistCompletion(handle, taskId, metadata = {}) {
1021
+ try {
1022
+ const result = await request('POST', '/api/onboarding/checklist', {
1023
+ handle,
1024
+ taskId,
1025
+ metadata
1026
+ });
1027
+ return result;
1028
+ } catch (e) {
1029
+ console.error('Track checklist completion failed:', e.message);
1030
+ return { success: false, error: e.message };
1031
+ }
1032
+ }
1033
+
1034
+ /**
1035
+ * Get onboarding data for a user (includes recommended builders)
1036
+ * @param {string} handle - User handle
1037
+ * @returns {Promise<{success: boolean, recommendedUsers: Array, ...}>}
1038
+ */
1039
+ async function getOnboardingData(handle) {
1040
+ try {
1041
+ const result = await request('GET', `/api/onboarding/data?handle=${encodeURIComponent(handle)}`);
1042
+ return result;
1043
+ } catch (e) {
1044
+ console.error('Get onboarding data failed:', e.message);
1045
+ return { success: false, error: e.message };
1046
+ }
1047
+ }
1048
+
760
1049
  // ============ ARTIFACTS ============
761
1050
 
762
1051
  /**
@@ -809,6 +1098,29 @@ async function getArtifact(slug) {
809
1098
  }
810
1099
  }
811
1100
 
1101
+ /**
1102
+ * Update an artifact by ID or slug
1103
+ * @param {string} idOrSlug - Artifact ID or slug
1104
+ * @param {Object} artifact - Updated artifact data
1105
+ */
1106
+ async function updateArtifact(idOrSlug, artifact) {
1107
+ try {
1108
+ const result = await request('PUT', `/api/artifacts/${idOrSlug}`, artifact);
1109
+
1110
+ if (result.success === false) {
1111
+ return { success: false, error: result.error || 'Update failed' };
1112
+ }
1113
+
1114
+ return {
1115
+ success: true,
1116
+ artifact: result.artifact
1117
+ };
1118
+ } catch (e) {
1119
+ console.error('Update artifact failed:', e.message);
1120
+ return { success: false, error: e.message };
1121
+ }
1122
+ }
1123
+
812
1124
  /**
813
1125
  * List artifacts
814
1126
  * @param {Object} options - { scope: 'mine'|'for-me'|'network', handle, limit }
@@ -874,6 +1186,11 @@ module.exports = {
874
1186
  getUnreadCount,
875
1187
  getThread,
876
1188
  markThreadRead,
1189
+ markMessagesDelivered,
1190
+ searchMessages,
1191
+
1192
+ // Watch Me Code
1193
+ getLiveBroadcastCount,
877
1194
 
878
1195
  // Consent
879
1196
  getConsentStatus,
@@ -903,12 +1220,16 @@ module.exports = {
903
1220
  // Artifacts
904
1221
  createArtifact,
905
1222
  getArtifact,
1223
+ updateArtifact,
906
1224
  listArtifacts,
907
1225
  sendArtifactCard,
908
1226
 
909
1227
  // Onboarding
910
1228
  getChecklistStatus,
1229
+ trackChecklistCompletion,
1230
+ getOnboardingData,
911
1231
 
912
1232
  // Auth
913
- verifyPrivyToken
1233
+ verifyAuthToken,
1234
+ verifyPrivyToken: verifyAuthToken // Backwards compat alias
914
1235
  };