slashvibe-mcp 0.3.21 → 0.3.22

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 (229) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +280 -47
  3. package/config.js +36 -31
  4. package/crypto.js +1 -6
  5. package/discord.js +19 -19
  6. package/index.js +217 -207
  7. package/intelligence/index.js +2 -9
  8. package/intelligence/infer.js +10 -16
  9. package/intelligence/patterns.js +23 -18
  10. package/intelligence/proactive.js +16 -15
  11. package/intelligence/serendipity.js +57 -20
  12. package/memory.js +13 -8
  13. package/notify.js +39 -14
  14. package/package.json +27 -20
  15. package/presence.js +2 -2
  16. package/prompts.js +5 -9
  17. package/protocol/index.js +123 -87
  18. package/protocol/telegram-commands.js +36 -37
  19. package/store/api.js +358 -529
  20. package/store/local.js +9 -10
  21. package/store/profiles.js +48 -192
  22. package/store/reservations.js +2 -9
  23. package/store/skills.js +69 -71
  24. package/store/sqlite.js +355 -0
  25. package/tools/_actions.js +48 -387
  26. package/tools/_connection-queue.js +45 -56
  27. package/tools/_discovery-enhanced.js +52 -57
  28. package/tools/_discovery.js +87 -185
  29. package/tools/{l2-status.js → _experimental/l2-status.js} +68 -70
  30. package/tools/{shipback.js → _experimental/shipback.js} +4 -3
  31. package/tools/_proactive-discovery.js +60 -73
  32. package/tools/_shared/index.js +41 -64
  33. package/tools/admin-inbox.js +10 -15
  34. package/tools/agents.js +1 -1
  35. package/tools/artifact-create.js +13 -23
  36. package/tools/artifact-view.js +4 -4
  37. package/tools/{_deprecated/back.js → back.js} +1 -1
  38. package/tools/bye.js +3 -5
  39. package/tools/consent.js +2 -2
  40. package/tools/context.js +9 -10
  41. package/tools/crossword.js +3 -2
  42. package/tools/discover.js +94 -356
  43. package/tools/dm.js +27 -86
  44. package/tools/doctor.js +12 -41
  45. package/tools/drawing.js +34 -20
  46. package/tools/echo.js +11 -11
  47. package/tools/feed.js +30 -58
  48. package/tools/follow.js +64 -187
  49. package/tools/{_deprecated/forget.js → forget.js} +4 -7
  50. package/tools/game.js +144 -48
  51. package/tools/handoff.js +6 -8
  52. package/tools/help.js +3 -3
  53. package/tools/idea.js +15 -27
  54. package/tools/inbox.js +121 -293
  55. package/tools/init.js +54 -151
  56. package/tools/invite.js +8 -21
  57. package/tools/migrate.js +27 -24
  58. package/tools/multiplayer-game.js +50 -40
  59. package/tools/{_deprecated/mute.js → mute.js} +4 -3
  60. package/tools/notifications.js +58 -48
  61. package/tools/observe.js +12 -15
  62. package/tools/onboarding.js +8 -11
  63. package/tools/open.js +13 -144
  64. package/tools/party-game.js +23 -12
  65. package/tools/patterns.js +2 -1
  66. package/tools/ping.js +5 -7
  67. package/tools/react.js +28 -30
  68. package/tools/{_deprecated/recall.js → recall.js} +5 -10
  69. package/tools/release.js +4 -2
  70. package/tools/{_deprecated/remember.js → remember.js} +4 -6
  71. package/tools/report.js +2 -2
  72. package/tools/request.js +6 -26
  73. package/tools/reserve.js +1 -1
  74. package/tools/session-fork.js +97 -0
  75. package/tools/session-save.js +109 -0
  76. package/tools/settings.js +30 -99
  77. package/tools/ship.js +74 -56
  78. package/tools/{_deprecated/skills-exchange.js → skills-exchange.js} +38 -39
  79. package/tools/social-inbox.js +22 -28
  80. package/tools/social-post.js +24 -27
  81. package/tools/solo-game.js +54 -46
  82. package/tools/start.js +14 -148
  83. package/tools/status.js +21 -68
  84. package/tools/submit.js +4 -2
  85. package/tools/suggest-tags.js +36 -33
  86. package/tools/summarize.js +19 -16
  87. package/tools/tag-suggestions.js +72 -73
  88. package/tools/test.js +1 -1
  89. package/tools/{_deprecated/tictactoe.js → tictactoe.js} +26 -26
  90. package/tools/token.js +4 -4
  91. package/tools/update.js +1 -2
  92. package/tools/watch.js +132 -112
  93. package/tools/who.js +20 -40
  94. package/tools/{_deprecated/wordassociation.js → wordassociation.js} +23 -20
  95. package/tools/workshop-buddy.js +52 -53
  96. package/tools/x-mentions.js +0 -1
  97. package/tools/x-reply.js +0 -1
  98. package/twitter.js +14 -20
  99. package/version.json +8 -10
  100. package/analytics.js +0 -107
  101. package/auth-store.js +0 -148
  102. package/auto-update.js +0 -130
  103. package/bridges/bridge-monitor.js +0 -388
  104. package/bridges/discord-bot.js +0 -431
  105. package/bridges/farcaster.js +0 -299
  106. package/bridges/telegram.js +0 -261
  107. package/bridges/webhook-health.js +0 -420
  108. package/bridges/webhook-server.js +0 -437
  109. package/bridges/whatsapp.js +0 -441
  110. package/bridges/x-webhook.js +0 -423
  111. package/games/arcade.js +0 -406
  112. package/games/chess.js +0 -451
  113. package/games/colorguess.js +0 -343
  114. package/games/crossword-words.js +0 -171
  115. package/games/crossword.js +0 -461
  116. package/games/drawing.js +0 -347
  117. package/games/gameroulette.js +0 -300
  118. package/games/gamerouter.js +0 -336
  119. package/games/gamestatus.js +0 -337
  120. package/games/guessnumber.js +0 -209
  121. package/games/hangman.js +0 -279
  122. package/games/memory.js +0 -338
  123. package/games/multiplayer-tictactoe.js +0 -389
  124. package/games/pixelart.js +0 -399
  125. package/games/quickduel.js +0 -354
  126. package/games/riddle.js +0 -371
  127. package/games/rockpaperscissors.js +0 -291
  128. package/games/snake.js +0 -406
  129. package/games/storybuilder.js +0 -343
  130. package/games/tictactoe.js +0 -345
  131. package/games/twentyquestions.js +0 -286
  132. package/games/twotruths.js +0 -207
  133. package/games/werewolf.js +0 -508
  134. package/games/wordassociation.js +0 -247
  135. package/games/wordchain.js +0 -135
  136. package/intelligence/interests.js +0 -369
  137. package/notification-emitter.js +0 -77
  138. package/setup.js +0 -480
  139. package/smart-inbox.js +0 -276
  140. package/tools/_deprecated/auto-suggest-connections.js +0 -304
  141. package/tools/_deprecated/bootstrap-skills.js +0 -231
  142. package/tools/_deprecated/bridge-dashboard.js +0 -342
  143. package/tools/_deprecated/bridge-health.js +0 -400
  144. package/tools/_deprecated/bridge-live.js +0 -384
  145. package/tools/_deprecated/bridges.js +0 -383
  146. package/tools/_deprecated/colorguess.js +0 -281
  147. package/tools/_deprecated/discover-insights.js +0 -379
  148. package/tools/_deprecated/discover-momentum.js +0 -256
  149. package/tools/_deprecated/discovery-analytics.js +0 -345
  150. package/tools/_deprecated/discovery-auto-suggest.js +0 -275
  151. package/tools/_deprecated/discovery-bootstrap.js +0 -267
  152. package/tools/_deprecated/discovery-daily.js +0 -375
  153. package/tools/_deprecated/discovery-dashboard.js +0 -385
  154. package/tools/_deprecated/discovery-digest.js +0 -314
  155. package/tools/_deprecated/discovery-hub.js +0 -357
  156. package/tools/_deprecated/discovery-insights.js +0 -384
  157. package/tools/_deprecated/discovery-momentum.js +0 -281
  158. package/tools/_deprecated/discovery-monitor.js +0 -319
  159. package/tools/_deprecated/discovery-proactive.js +0 -300
  160. package/tools/_deprecated/draw.js +0 -317
  161. package/tools/_deprecated/farcaster.js +0 -307
  162. package/tools/_deprecated/games-catalog.js +0 -376
  163. package/tools/_deprecated/games.js +0 -313
  164. package/tools/_deprecated/guessnumber.js +0 -194
  165. package/tools/_deprecated/hangman.js +0 -129
  166. package/tools/_deprecated/multiplayer-tictactoe.js +0 -303
  167. package/tools/_deprecated/riddle.js +0 -240
  168. package/tools/_deprecated/run-bootstrap.js +0 -69
  169. package/tools/_deprecated/skills-analytics.js +0 -349
  170. package/tools/_deprecated/skills-bootstrap.js +0 -301
  171. package/tools/_deprecated/skills-dashboard.js +0 -268
  172. package/tools/_deprecated/skills.js +0 -380
  173. package/tools/_deprecated/smart-intro.js +0 -353
  174. package/tools/_deprecated/storybuilder.js +0 -331
  175. package/tools/_deprecated/telegram-bot.js +0 -183
  176. package/tools/_deprecated/telegram-setup.js +0 -214
  177. package/tools/_deprecated/twentyquestions.js +0 -143
  178. package/tools/_shared.js +0 -234
  179. package/tools/_work-context.js +0 -338
  180. package/tools/_work-context.manual-test.js +0 -199
  181. package/tools/_work-context.test.js +0 -260
  182. package/tools/activity.js +0 -220
  183. package/tools/agent-treasury.js +0 -288
  184. package/tools/analytics.js +0 -191
  185. package/tools/approve.js +0 -197
  186. package/tools/arcade.js +0 -173
  187. package/tools/artifacts-price.js +0 -107
  188. package/tools/ask-expert.js +0 -160
  189. package/tools/available.js +0 -120
  190. package/tools/become-expert.js +0 -150
  191. package/tools/broadcast.js +0 -325
  192. package/tools/chat.js +0 -202
  193. package/tools/collaborative-drawing.js +0 -286
  194. package/tools/connection-status.js +0 -178
  195. package/tools/earnings.js +0 -126
  196. package/tools/friends.js +0 -207
  197. package/tools/genesis.js +0 -233
  198. package/tools/gig-browse.js +0 -206
  199. package/tools/gig-complete.js +0 -144
  200. package/tools/health.js +0 -87
  201. package/tools/leaderboard.js +0 -117
  202. package/tools/lib/git-apply.js +0 -206
  203. package/tools/lib/git-bundle.js +0 -407
  204. package/tools/mint.js +0 -377
  205. package/tools/plan.js +0 -225
  206. package/tools/profile.js +0 -219
  207. package/tools/proof-of-work.js +0 -144
  208. package/tools/pulse.js +0 -218
  209. package/tools/reply.js +0 -166
  210. package/tools/reputation.js +0 -175
  211. package/tools/schedule.js +0 -367
  212. package/tools/search-messages.js +0 -123
  213. package/tools/session.js +0 -467
  214. package/tools/session_price.js +0 -128
  215. package/tools/smart-check.js +0 -201
  216. package/tools/social-processor.js +0 -445
  217. package/tools/streak.js +0 -147
  218. package/tools/stuck.js +0 -297
  219. package/tools/subscribe.js +0 -148
  220. package/tools/subscriptions.js +0 -134
  221. package/tools/tip.js +0 -193
  222. package/tools/wallet.js +0 -269
  223. package/tools/webhook-test.js +0 -388
  224. package/tools/withdraw.js +0 -145
  225. package/tools/work-summary.js +0 -96
  226. package/tools/workshop.js +0 -327
  227. /package/tools/{l2-bridge.js → _experimental/l2-bridge.js} +0 -0
  228. /package/tools/{l2.js → _experimental/l2.js} +0 -0
  229. /package/tools/{_deprecated/away.js → away.js} +0 -0
package/store/api.js CHANGED
@@ -10,58 +10,14 @@ 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
+ const sqlite = require('./sqlite'); // V2 messaging - local persistence
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
- // 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 = {}) {
20
+ function request(method, path, data = null, options = {}) {
65
21
  return new Promise((resolve, reject) => {
66
22
  const url = new URL(path, API_URL);
67
23
  const isHttps = url.protocol === 'https:';
@@ -73,9 +29,8 @@ function requestOnce(method, path, data = null, options = {}) {
73
29
  'User-Agent': 'vibe-mcp/1.0'
74
30
  };
75
31
 
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();
32
+ // Add auth token if provided or if we have one stored
33
+ const token = options.token || config.getAuthToken();
79
34
  if (token && options.auth !== false) {
80
35
  headers['Authorization'] = `Bearer ${token}`;
81
36
  }
@@ -89,48 +44,12 @@ function requestOnce(method, path, data = null, options = {}) {
89
44
  timeout
90
45
  };
91
46
 
92
- const req = client.request(reqOptions, (res) => {
47
+ const req = client.request(reqOptions, res => {
93
48
  let body = '';
94
- res.on('data', chunk => body += chunk);
95
- res.on('end', async () => {
49
+ res.on('data', chunk => (body += chunk));
50
+ res.on('end', () => {
96
51
  // Handle non-2xx responses
97
52
  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
-
134
53
  try {
135
54
  const parsed = JSON.parse(body);
136
55
  resolve({ success: false, error: parsed.error || `HTTP ${res.statusCode}`, statusCode: res.statusCode });
@@ -151,59 +70,24 @@ function requestOnce(method, path, data = null, options = {}) {
151
70
  // Handle timeout
152
71
  req.on('timeout', () => {
153
72
  req.destroy();
154
- resolve({ success: false, error: 'Request timeout', timeout: true, retryable: true });
73
+ resolve({ success: false, error: 'Request timeout', timeout: true });
155
74
  });
156
75
 
157
- req.on('error', (e) => {
158
- resolve({ success: false, error: e.message, network: true, retryable: true });
76
+ req.on('error', e => {
77
+ resolve({ success: false, error: e.message, network: true });
159
78
  });
160
79
 
161
80
  if (data) {
162
- req.write(JSON.stringify(data));
81
+ const payload = JSON.stringify(data);
82
+ req.setHeader('Content-Length', Buffer.byteLength(payload));
83
+ req.write(payload);
163
84
  }
164
85
  req.end();
165
86
  });
166
87
  }
167
88
 
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
-
202
89
  // ============ PRESENCE ============
203
90
 
204
- // Use v2 API by default (Postgres-backed)
205
- const USE_V2_PRESENCE = process.env.VIBE_PRESENCE_V1 !== 'true';
206
-
207
91
  // Session ID for this MCP instance
208
92
  let currentSessionId = null;
209
93
 
@@ -227,14 +111,14 @@ async function registerSession(sessionId, handle, building = null, publicKey = n
227
111
  registrationData.publicKey = publicKey;
228
112
  }
229
113
 
230
- const result = await request('POST', '/api/presence', registrationData, { auth: false }); // Don't send token for registration (we don't have one yet)
114
+ const result = await request('POST', '/api/presence', registrationData, { auth: false }); // Don't send token for registration (we don't have one yet)
231
115
 
232
116
  if (result.success && result.token) {
233
117
  // Use server-issued sessionId and token (not client-generated)
234
118
  currentSessionId = result.sessionId;
235
119
 
236
120
  // Save token for future authenticated requests (persist to shared config)
237
- config.saveAuthToken(result.token);
121
+ config.savePrivyToken(result.token);
238
122
 
239
123
  console.error(`[vibe] Registered @${handle} with session ${result.sessionId}`);
240
124
  } else if (result.success) {
@@ -243,7 +127,7 @@ async function registerSession(sessionId, handle, building = null, publicKey = n
243
127
  console.error(`[vibe] Registered @${handle} (legacy mode)`);
244
128
  }
245
129
 
246
- // Also register user in users DB (for @seth welcome tracking)
130
+ // Also register user in users DB (for @vibe welcome tracking)
247
131
  // AIRC: Include public key for identity
248
132
  try {
249
133
  const userData = {
@@ -253,7 +137,7 @@ async function registerSession(sessionId, handle, building = null, publicKey = n
253
137
  if (publicKey) {
254
138
  userData.publicKey = publicKey;
255
139
  }
256
- await request('POST', '/api/users', userData, { auth: false }); // User registration doesn't need auth
140
+ await request('POST', '/api/users', userData, { auth: false }); // User registration doesn't need auth
257
141
  } catch (e) {
258
142
  // Non-fatal if user registration fails
259
143
  }
@@ -265,11 +149,10 @@ async function registerSession(sessionId, handle, building = null, publicKey = n
265
149
  }
266
150
  }
267
151
 
268
- async function heartbeat(handle, one_liner, context = null) {
152
+ async function heartbeat(handle, one_liner, context = null, source = null) {
269
153
  try {
270
- const endpoint = USE_V2_PRESENCE ? '/api/v2/presence' : '/api/presence';
271
-
272
- // Build payload
154
+ // Token-based auth: server extracts handle from token
155
+ // Only need to send workingOn and context
273
156
  const payload = { workingOn: one_liner };
274
157
 
275
158
  // Fallback: if no token, send username (legacy support)
@@ -277,23 +160,18 @@ async function heartbeat(handle, one_liner, context = null) {
277
160
  payload.username = handle;
278
161
  }
279
162
 
280
- // Add context fields (v2 flattens these)
163
+ // Add context (mood, file, etc.) if provided
281
164
  if (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
- }
165
+ payload.context = context;
166
+ }
167
+
168
+ // Phase 1 Presence Bridge: track which surface is reporting
169
+ // See VIBE_CLAWDBOT_INTEGRATION_SPEC.md
170
+ if (source) {
171
+ payload.source = source;
294
172
  }
295
173
 
296
- await request('POST', endpoint, payload);
174
+ await request('POST', '/api/presence', payload);
297
175
  } catch (e) {
298
176
  console.error('Heartbeat failed:', e.message);
299
177
  }
@@ -301,21 +179,24 @@ async function heartbeat(handle, one_liner, context = null) {
301
179
 
302
180
  async function sendTypingIndicator(handle, toHandle) {
303
181
  try {
304
- // Use dedicated typing API with 5s TTL
305
- await request('POST', '/api/typing', {
306
- from: handle,
307
- to: toHandle,
308
- });
182
+ // Token auth: server extracts sender from token
183
+ const payload = { typingTo: toHandle };
184
+
185
+ // Fallback for legacy
186
+ if (!config.getAuthToken()) {
187
+ payload.username = handle;
188
+ }
189
+
190
+ await request('POST', '/api/presence', payload);
309
191
  } catch (e) {
310
- // Silent fail - typing is best-effort
192
+ console.error('Typing indicator failed:', e.message);
311
193
  }
312
194
  }
313
195
 
314
196
  async function getTypingUsers(forHandle) {
315
197
  try {
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 || [];
198
+ const result = await request('GET', `/api/presence?user=${forHandle}&typing=true`);
199
+ return result.typingUsers || [];
319
200
  } catch (e) {
320
201
  return [];
321
202
  }
@@ -323,55 +204,38 @@ async function getTypingUsers(forHandle) {
323
204
 
324
205
  async function getActiveUsers() {
325
206
  try {
326
- const endpoint = USE_V2_PRESENCE ? '/api/v2/presence' : '/api/presence';
327
- const result = await request('GET', endpoint);
328
-
207
+ const result = await request('GET', '/api/presence');
329
208
  // Combine active and away users
330
209
  const users = [...(result.active || []), ...(result.away || [])];
331
-
332
- // Map to normalized format (v2 uses 'handle', v1 uses 'username')
333
- const mappedUsers = users.map(u => ({
334
- handle: u.handle || u.username,
210
+ return users.map(u => ({
211
+ handle: u.username,
335
212
  one_liner: u.workingOn,
336
213
  lastSeen: new Date(u.lastSeen).getTime(),
337
214
  firstSeen: u.firstSeen ? new Date(u.firstSeen).getTime() : null,
338
215
  status: u.status,
339
- // Mood: v2 is flat, v1 is nested
340
- mood: u.mood || u.context?.mood || null,
216
+ // Mood: explicit (context.mood) or inferred (u.mood)
217
+ mood: u.context?.mood || u.mood || null,
341
218
  mood_inferred: u.mood_inferred || false,
342
219
  mood_reason: u.mood_reason || null,
343
220
  builderMode: u.builderMode || null,
344
221
  // Context sharing fields
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,
222
+ file: u.context?.file || null,
223
+ branch: u.context?.branch || null,
224
+ repo: u.context?.repo || null,
225
+ error: u.context?.error || null,
226
+ note: u.context?.note || null,
350
227
  // Away status
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
228
+ awayMessage: u.context?.awayMessage || null,
229
+ awayAt: u.context?.awayAt || null,
230
+ // Agent fields (used by who.js for badges)
231
+ is_agent: u.isAgent || false,
232
+ operator: u.operator || null,
233
+ // GitHub activity (used by who.js heat detection)
234
+ github: u.github || null,
235
+ // Phase 1 Presence Bridge: multi-source tracking
236
+ sources: u.sources || null,
237
+ reach_via: u.reach_via || null
356
238
  }));
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;
375
239
  } catch (e) {
376
240
  console.error('Who failed:', e.message);
377
241
  return [];
@@ -379,68 +243,57 @@ async function getActiveUsers() {
379
243
  }
380
244
 
381
245
  async function setVisibility(handle, visible) {
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
- }
246
+ // TODO: implement visibility toggle API
399
247
  }
400
248
 
401
249
  // ============ MESSAGES ============
402
250
 
403
- // Use v2 API by default (Postgres-backed, cross-client sync)
404
- const USE_V2_MESSAGES = process.env.VIBE_MESSAGES_V1 !== 'true';
251
+ async function sendMessage(from, to, body, type = 'dm', payload = null) {
252
+ // V2 MESSAGING: Save to SQLite first (optimistic UI)
253
+ const local_id = require('crypto').randomUUID();
254
+ const created_at = new Date().toISOString();
255
+
256
+ try {
257
+ // 1. Save to local SQLite (optimistic - before API call)
258
+ sqlite.saveLocalMessage({
259
+ local_id,
260
+ from_handle: from,
261
+ to_handle: to,
262
+ content: body || '',
263
+ created_at,
264
+ status: 'pending'
265
+ });
266
+ } catch (sqliteError) {
267
+ // Don't fail message send if SQLite fails (just log)
268
+ console.warn('[SQLite] Failed to save message locally:', sqliteError.message);
269
+ }
405
270
 
406
- async function sendMessage(from, to, body, type = 'dm', payload = null, options = {}) {
407
271
  try {
408
272
  let data;
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
273
+
274
+ // Check if using Privy auth (server-side signing)
275
+ if (config.hasPrivyAuth()) {
276
+ // NEW: Privy auth flow - server handles signing
427
277
  // Just send message data, server signs it
428
278
  data = { to, body: body || undefined, text: body };
429
279
  if (payload) data.payload = payload;
430
280
 
431
- console.error('[vibe] Sending message via OAuth (server-side signing)');
281
+ console.error('[vibe] Sending message via Privy auth (server-side signing)');
432
282
  } else {
433
283
  // LEGACY: Create signed message if we have a keypair
434
284
  const keypair = config.getKeypair();
435
285
 
436
286
  if (keypair) {
437
287
  // Full AIRC-compliant signed message
438
- data = crypto.createSignedMessage({
439
- from,
440
- to,
441
- body: body || undefined,
442
- payload: payload || undefined
443
- }, keypair.privateKey);
288
+ data = crypto.createSignedMessage(
289
+ {
290
+ from,
291
+ to,
292
+ body: body || undefined,
293
+ payload: payload || undefined
294
+ },
295
+ keypair.privateKey
296
+ );
444
297
 
445
298
  // Also include 'text' for backward compat with current API
446
299
  if (body) data.text = body;
@@ -453,34 +306,61 @@ async function sendMessage(from, to, body, type = 'dm', payload = null, options
453
306
  }
454
307
  }
455
308
 
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));
309
+ const result = await request('POST', '/api/messages', data);
459
310
 
460
311
  // Handle auth errors
461
312
  if (!result.success && result.error?.includes('Authentication')) {
313
+ // Mark as failed in SQLite
314
+ try {
315
+ sqlite.updateMessageStatus(local_id, 'failed');
316
+ } catch (e) {}
462
317
  console.error('[vibe] Auth failed for message. Try `vibe init` to re-register.');
463
318
  return { error: 'auth_failed', message: 'Authentication failed. Try `vibe init` to re-register.' };
464
319
  }
465
320
 
466
321
  // Handle expired token
467
322
  if (result.statusCode === 401) {
323
+ // Mark as failed in SQLite
324
+ try {
325
+ sqlite.updateMessageStatus(local_id, 'failed');
326
+ } catch (e) {}
468
327
  console.error('[vibe] Auth expired. Run browser auth to refresh token.');
469
328
  return { error: 'auth_expired', message: 'Auth expired. Run `vibe init` to refresh token.' };
470
329
  }
471
330
 
472
331
  // Handle storage errors (KV write failed)
473
332
  if (!result.success && result.error === 'storage_error') {
333
+ // Mark as failed in SQLite
334
+ try {
335
+ sqlite.updateMessageStatus(local_id, 'failed');
336
+ } catch (e) {}
474
337
  console.error('[vibe] Storage error:', result.details || result.message);
475
338
  return { error: 'storage_error', message: result.message || 'Failed to save message. Please try again.' };
476
339
  }
477
340
 
478
341
  // Handle other errors
479
342
  if (!result.success && result.error) {
343
+ // Mark as failed in SQLite
344
+ try {
345
+ sqlite.updateMessageStatus(local_id, 'failed');
346
+ } catch (e) {}
480
347
  console.error('[vibe] Send error:', result.error, result.message);
481
348
  return { error: result.error, message: result.message || 'Failed to send message.' };
482
349
  }
483
350
 
351
+ // V2 MESSAGING: Update SQLite with server_id, thread_id and mark as sent
352
+ if (result.success || result.message) {
353
+ try {
354
+ // V2 Postgres: result.message.id, result.message.thread_id
355
+ const message = result.message || {};
356
+ const server_id = message.id || result.messageId || result.id || null;
357
+ const thread_id = message.thread_id || null;
358
+ sqlite.updateMessageStatus(local_id, 'sent', server_id, thread_id);
359
+ } catch (sqliteError) {
360
+ console.warn('[SQLite] Failed to update message status:', sqliteError.message);
361
+ }
362
+ }
363
+
484
364
  // Emit list_changed notification for successful message send
485
365
  // This allows other Claude Code instances to see the new message instantly
486
366
  if (result.success || result.message) {
@@ -492,96 +372,100 @@ async function sendMessage(from, to, body, type = 'dm', payload = null, options
492
372
  return result.message;
493
373
  } catch (e) {
494
374
  console.error('Send failed:', e.message);
375
+ // Mark as failed in SQLite
376
+ try {
377
+ sqlite.updateMessageStatus(local_id, 'failed');
378
+ } catch (sqliteErr) {}
495
379
  return null;
496
380
  }
497
381
  }
498
382
 
499
383
  async function getInbox(handle) {
500
384
  try {
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
- }
385
+ // V2 MESSAGING: Hybrid approach - SQLite (fast) + API (sync)
510
386
 
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
- }));
387
+ // 1. Get from local SQLite first
388
+ let localInbox = [];
389
+ try {
390
+ localInbox = sqlite.getInboxThreads(handle);
391
+ } catch (sqliteError) {
392
+ console.warn('[SQLite] Failed to read inbox:', sqliteError.message);
520
393
  }
521
394
 
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 }
395
+ // 2. Fetch from API (sync with backend)
534
396
  const result = await request('GET', `/api/messages?user=${handle}`);
535
397
 
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
- }
398
+ // V2 Postgres: result.threads[] with thread_id
399
+ const threads = result.threads || [];
541
400
 
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
- }));
401
+ // Merge threads into SQLite for persistence
402
+ if (threads.length > 0) {
403
+ try {
404
+ threads.forEach(thread => {
405
+ const msg = thread.last_message;
406
+ if (msg) {
407
+ sqlite.mergeServerMessages([
408
+ {
409
+ server_id: msg.id,
410
+ thread_id: thread.id, // V2 thread_id
411
+ from_handle: msg.from,
412
+ to_handle: handle === msg.from ? thread.with : handle,
413
+ content: msg.body,
414
+ created_at: msg.created_at,
415
+ status: 'delivered'
416
+ }
417
+ ]);
418
+ }
419
+ });
420
+ } catch (sqliteError) {
421
+ console.warn('[SQLite] Failed to merge inbox threads:', sqliteError.message);
422
+ }
552
423
  }
553
424
 
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()
425
+ // Return V2 format
426
+ return threads.map(thread => ({
427
+ handle: thread.with,
428
+ messages: thread.last_message
429
+ ? [
430
+ {
431
+ from: thread.last_message.from,
432
+ body: thread.last_message.body,
433
+ timestamp: new Date(thread.last_message.created_at).getTime(),
434
+ read: thread.unread === 0
435
+ }
436
+ ]
437
+ : [],
438
+ unread: thread.unread,
439
+ lastMessage: thread.last_message?.body,
440
+ lastTimestamp: thread.last_message ? new Date(thread.last_message.created_at).getTime() : 0
567
441
  }));
568
442
  } catch (e) {
569
- console.error('Inbox v1 failed:', e.message);
570
- return [];
443
+ console.error('Inbox failed:', e.message);
444
+
445
+ // Fallback to SQLite if API fails
446
+ try {
447
+ const localInbox = sqlite.getInboxThreads(handle);
448
+ return localInbox.map(thread => ({
449
+ handle: thread.partner,
450
+ messages: [thread.latestMessage].map(m => ({
451
+ from: m.from_handle,
452
+ body: m.content,
453
+ timestamp: new Date(m.created_at).getTime(),
454
+ read: m.status === 'read'
455
+ })),
456
+ unread: thread.unreadCount,
457
+ lastMessage: thread.latestMessage.content,
458
+ lastTimestamp: new Date(thread.latestMessage.created_at).getTime()
459
+ }));
460
+ } catch (sqliteError) {
461
+ return [];
462
+ }
571
463
  }
572
464
  }
573
465
 
574
466
  async function getUnreadCount(handle) {
575
467
  try {
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
468
+ // Use unified messages endpoint - returns { inbox, unread, bySender }
585
469
  const result = await request('GET', `/api/messages?user=${handle}`);
586
470
  return result.unread || 0;
587
471
  } catch (e) {
@@ -589,196 +473,99 @@ async function getUnreadCount(handle) {
589
473
  }
590
474
  }
591
475
 
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
-
608
476
  // 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
611
477
  async function getRawInbox(handle) {
612
478
  try {
613
- // V2: Get threads with unread counts (read state synced via Postgres)
479
+ // Use unified messages endpoint - returns { inbox, unread, bySender }
614
480
  const result = await request('GET', `/api/messages?user=${handle}`);
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;
481
+ return result.inbox || [];
641
482
  } catch (e) {
642
- console.error('[getRawInbox] Error:', e.message);
643
483
  return [];
644
484
  }
645
485
  }
646
486
 
647
487
  async function getThread(myHandle, theirHandle) {
648
488
  try {
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
- }
489
+ // V2 MESSAGING: Hybrid approach - SQLite (fast) + API (sync)
682
490
 
683
- // Fall back to v1 if v2 fails or thread not found
684
- console.error('[getThread] V2 failed, falling back to v1');
491
+ // 1. Get from local SQLite first (instant, works offline)
492
+ let localMessages = [];
493
+ try {
494
+ localMessages = sqlite.getThreadMessages(myHandle, theirHandle);
495
+ } catch (sqliteError) {
496
+ console.warn('[SQLite] Failed to read thread:', sqliteError.message);
685
497
  }
686
498
 
687
- // V1 fallback
499
+ // 2. Fetch from API (sync with backend)
688
500
  const result = await request('GET', `/api/messages?user=${myHandle}&with=${theirHandle}`);
689
- return (result.thread || []).map(m => ({
501
+
502
+ // V2 Postgres: result.messages[] (not result.thread)
503
+ const apiMessages = result.messages || result.thread || [];
504
+
505
+ // 3. Merge API messages into SQLite (for future reads)
506
+ if (apiMessages.length > 0) {
507
+ try {
508
+ sqlite.mergeServerMessages(
509
+ apiMessages.map(m => ({
510
+ server_id: m.id || m.messageId,
511
+ thread_id: m.thread_id || null, // V2 thread_id (if present)
512
+ from_handle: m.from,
513
+ to_handle: m.to || (m.from === myHandle ? theirHandle : myHandle),
514
+ content: m.body || m.text || '', // V2 uses 'body'
515
+ created_at: m.created_at || m.createdAt || new Date().toISOString(),
516
+ status: 'delivered',
517
+ sent_at: m.sent_at || m.sentAt || m.created_at || m.createdAt,
518
+ delivered_at: m.delivered_at || m.deliveredAt || m.created_at || m.createdAt
519
+ }))
520
+ );
521
+ } catch (sqliteError) {
522
+ console.warn('[SQLite] Failed to merge messages:', sqliteError.message);
523
+ }
524
+ }
525
+
526
+ // 4. Return merged result (prefer API for latest, fallback to local)
527
+ const messages =
528
+ apiMessages.length > 0
529
+ ? apiMessages
530
+ : localMessages.map(m => ({
531
+ id: m.server_id,
532
+ from: m.from_handle,
533
+ to: m.to_handle,
534
+ body: m.content, // V2 uses 'body'
535
+ created_at: m.created_at
536
+ }));
537
+
538
+ return messages.map(m => ({
690
539
  from: m.from,
691
- isAgent: m.isAgent || m.is_agent || false, // Support both naming conventions
692
- body: m.text,
540
+ isAgent: m.isAgent || m.is_agent || false,
541
+ body: m.body || m.text || m.content || '', // V2: m.body, fallback to legacy
693
542
  payload: m.payload || null,
694
- timestamp: new Date(m.createdAt).getTime(),
543
+ timestamp: new Date(m.created_at || m.createdAt).getTime(),
695
544
  direction: m.direction
696
545
  }));
697
546
  } catch (e) {
698
547
  console.error('Thread failed:', e.message);
699
- return [];
700
- }
701
- }
702
548
 
703
- async function markThreadRead(myHandle, theirHandle, lastMessageId = null) {
704
- // V2: Explicit mark read via PATCH
705
- if (USE_V2_MESSAGES && lastMessageId) {
549
+ // Fallback to SQLite if API fails
706
550
  try {
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);
551
+ const localMessages = sqlite.getThreadMessages(myHandle, theirHandle);
552
+ return localMessages.map(m => ({
553
+ from: m.from_handle,
554
+ isAgent: false,
555
+ body: m.content,
556
+ payload: null,
557
+ timestamp: new Date(m.created_at).getTime(),
558
+ direction: m.from_handle === myHandle ? 'sent' : 'received'
559
+ }));
560
+ } catch (sqliteError) {
561
+ return [];
723
562
  }
724
563
  }
725
-
726
- // V1: No-op - Backend automatically marks messages as read when getThread() is called
727
- // See: api/messages.js thread endpoint (GET /api/messages?user=X&with=Y)
728
564
  }
729
565
 
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
- }
566
+ async function markThreadRead(myHandle, theirHandle) {
567
+ // No-op: Backend automatically marks messages as read when getThread() is called
568
+ // See: api/messages.js thread endpoint (GET /api/messages?user=X&with=Y)
782
569
  }
783
570
 
784
571
  // ============ CONSENT ============
@@ -889,11 +676,11 @@ async function checkInviteCode(code) {
889
676
  // ============ AUTH ============
890
677
 
891
678
  /**
892
- * Verify an auth token with the server
893
- * @param {string} token - JWT token from GitHub OAuth
679
+ * Verify a Privy token with the server
680
+ * @param {string} token - Privy JWT token
894
681
  * @returns {Promise<{valid: boolean, handle?: string, error?: string}>}
895
682
  */
896
- async function verifyAuthToken(token) {
683
+ async function verifyPrivyToken(token) {
897
684
  try {
898
685
  const result = await request('POST', '/api/auth/verify', {}, { token, auth: true });
899
686
 
@@ -1010,38 +797,101 @@ async function getChecklistStatus(handle) {
1010
797
  }
1011
798
  }
1012
799
 
800
+ // ============ FOLLOW ============
801
+
1013
802
  /**
1014
- * Track completion of an onboarding task
803
+ * Follow a user
804
+ * @param {string} follower - Handle of the follower
805
+ * @param {string} following - Handle of the user to follow
806
+ */
807
+ async function followUser(follower, following) {
808
+ try {
809
+ const result = await request('POST', '/api/follow', { follower, following });
810
+ return result;
811
+ } catch (e) {
812
+ return { success: false, error: e.message };
813
+ }
814
+ }
815
+
816
+ /**
817
+ * Unfollow a user
818
+ * @param {string} follower - Handle of the follower
819
+ * @param {string} following - Handle of the user to unfollow
820
+ */
821
+ async function unfollowUser(follower, following) {
822
+ try {
823
+ const result = await request('DELETE', '/api/follow', { follower, following });
824
+ return result;
825
+ } catch (e) {
826
+ return { success: false, error: e.message };
827
+ }
828
+ }
829
+
830
+ /**
831
+ * Get list of users someone is following
1015
832
  * @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
833
  */
1020
- async function trackChecklistCompletion(handle, taskId, metadata = {}) {
834
+ async function getFollowing(handle) {
1021
835
  try {
1022
- const result = await request('POST', '/api/onboarding/checklist', {
1023
- handle,
1024
- taskId,
1025
- metadata
1026
- });
836
+ const result = await request('GET', `/api/following?handle=${encodeURIComponent(handle)}`);
1027
837
  return result;
1028
838
  } catch (e) {
1029
- console.error('Track checklist completion failed:', e.message);
1030
839
  return { success: false, error: e.message };
1031
840
  }
1032
841
  }
1033
842
 
1034
843
  /**
1035
- * Get onboarding data for a user (includes recommended builders)
844
+ * Get list of followers for a user
1036
845
  * @param {string} handle - User handle
1037
- * @returns {Promise<{success: boolean, recommendedUsers: Array, ...}>}
1038
846
  */
1039
- async function getOnboardingData(handle) {
847
+ async function getFollowers(handle) {
848
+ try {
849
+ const result = await request('GET', `/api/followers?handle=${encodeURIComponent(handle)}`);
850
+ return result;
851
+ } catch (e) {
852
+ return { success: false, error: e.message };
853
+ }
854
+ }
855
+
856
+ // ============ WATCH / LIVE ============
857
+
858
+ /**
859
+ * Get all active broadcasts
860
+ * @returns {Object} { success, broadcasts, upcoming, count }
861
+ */
862
+ async function getLiveBroadcasts() {
863
+ try {
864
+ const result = await request('GET', '/api/live?format=json');
865
+ return result;
866
+ } catch (e) {
867
+ return { success: false, error: e.message };
868
+ }
869
+ }
870
+
871
+ /**
872
+ * Get broadcast info for a specific room
873
+ * @param {string} roomId - Broadcast room ID
874
+ * @returns {Object} { success, broadcast }
875
+ */
876
+ async function getBroadcast(roomId) {
1040
877
  try {
1041
- const result = await request('GET', `/api/onboarding/data?handle=${encodeURIComponent(handle)}`);
878
+ const result = await request('GET', `/api/watch?room=${encodeURIComponent(roomId)}`);
879
+ return result;
880
+ } catch (e) {
881
+ return { success: false, error: e.message };
882
+ }
883
+ }
884
+
885
+ /**
886
+ * Get engagement metrics for a broadcast
887
+ * @param {string} roomId - Broadcast room ID
888
+ * @returns {Object} { success, viewers, chat, reactions, engagement }
889
+ */
890
+ async function getBroadcastMetrics(roomId) {
891
+ try {
892
+ const result = await request('GET', `/api/watch/metrics?room=${encodeURIComponent(roomId)}`);
1042
893
  return result;
1043
894
  } catch (e) {
1044
- console.error('Get onboarding data failed:', e.message);
1045
895
  return { success: false, error: e.message };
1046
896
  }
1047
897
  }
@@ -1098,29 +948,6 @@ async function getArtifact(slug) {
1098
948
  }
1099
949
  }
1100
950
 
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
-
1124
951
  /**
1125
952
  * List artifacts
1126
953
  * @param {Object} options - { scope: 'mine'|'for-me'|'network', handle, limit }
@@ -1186,11 +1013,6 @@ module.exports = {
1186
1013
  getUnreadCount,
1187
1014
  getThread,
1188
1015
  markThreadRead,
1189
- markMessagesDelivered,
1190
- searchMessages,
1191
-
1192
- // Watch Me Code
1193
- getLiveBroadcastCount,
1194
1016
 
1195
1017
  // Consent
1196
1018
  getConsentStatus,
@@ -1220,16 +1042,23 @@ module.exports = {
1220
1042
  // Artifacts
1221
1043
  createArtifact,
1222
1044
  getArtifact,
1223
- updateArtifact,
1224
1045
  listArtifacts,
1225
1046
  sendArtifactCard,
1226
1047
 
1227
1048
  // Onboarding
1228
1049
  getChecklistStatus,
1229
- trackChecklistCompletion,
1230
- getOnboardingData,
1050
+
1051
+ // Follow
1052
+ followUser,
1053
+ unfollowUser,
1054
+ getFollowing,
1055
+ getFollowers,
1056
+
1057
+ // Watch / Live
1058
+ getLiveBroadcasts,
1059
+ getBroadcast,
1060
+ getBroadcastMetrics,
1231
1061
 
1232
1062
  // Auth
1233
- verifyAuthToken,
1234
- verifyPrivyToken: verifyAuthToken // Backwards compat alias
1063
+ verifyPrivyToken
1235
1064
  };