slashvibe-mcp 0.3.21 → 0.3.23

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 (235) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +280 -47
  3. package/auto-update.js +10 -15
  4. package/config.js +36 -31
  5. package/crypto.js +1 -6
  6. package/debug.js +12 -0
  7. package/discord.js +19 -19
  8. package/eslint.config.js +54 -0
  9. package/index.js +217 -207
  10. package/intelligence/index.js +2 -9
  11. package/intelligence/infer.js +10 -16
  12. package/intelligence/patterns.js +23 -18
  13. package/intelligence/proactive.js +16 -15
  14. package/intelligence/serendipity.js +57 -20
  15. package/memory.js +13 -8
  16. package/migrate-v2.js +72 -0
  17. package/notification-emitter.js +2 -2
  18. package/notify.js +39 -14
  19. package/package.json +28 -29
  20. package/post-install.js +141 -0
  21. package/presence.js +2 -2
  22. package/prompts.js +5 -9
  23. package/protocol/index.js +123 -87
  24. package/protocol/telegram-commands.js +36 -37
  25. package/store/api.js +358 -529
  26. package/store/local.js +9 -10
  27. package/store/profiles.js +48 -192
  28. package/store/reservations.js +2 -9
  29. package/store/skills.js +69 -71
  30. package/store/sqlite.js +355 -0
  31. package/test-skills-bootstrap.js +20 -0
  32. package/test-v2-integration.js +385 -0
  33. package/tools/_actions.js +48 -387
  34. package/tools/_connection-queue.js +45 -56
  35. package/tools/_discovery-enhanced.js +52 -57
  36. package/tools/_discovery.js +87 -185
  37. package/tools/{l2-status.js → _experimental/l2-status.js} +68 -70
  38. package/tools/{shipback.js → _experimental/shipback.js} +4 -3
  39. package/tools/_proactive-discovery.js +60 -73
  40. package/tools/_shared/index.js +41 -64
  41. package/tools/admin-inbox.js +10 -15
  42. package/tools/agents.js +1 -1
  43. package/tools/artifact-create.js +13 -23
  44. package/tools/artifact-view.js +4 -4
  45. package/tools/{_deprecated/back.js → back.js} +1 -1
  46. package/tools/bye.js +3 -5
  47. package/tools/consent.js +2 -2
  48. package/tools/context.js +9 -10
  49. package/tools/crossword.js +3 -2
  50. package/tools/discover.js +94 -356
  51. package/tools/dm.js +27 -86
  52. package/tools/doctor.js +12 -41
  53. package/tools/drawing.js +34 -20
  54. package/tools/echo.js +11 -11
  55. package/tools/feed.js +30 -58
  56. package/tools/follow.js +64 -187
  57. package/tools/{_deprecated/forget.js → forget.js} +4 -7
  58. package/tools/game.js +144 -48
  59. package/tools/handoff.js +6 -8
  60. package/tools/help.js +3 -3
  61. package/tools/idea.js +15 -27
  62. package/tools/inbox.js +121 -293
  63. package/tools/init.js +54 -151
  64. package/tools/invite.js +8 -21
  65. package/tools/migrate.js +27 -24
  66. package/tools/multiplayer-game.js +50 -40
  67. package/tools/{_deprecated/mute.js → mute.js} +4 -3
  68. package/tools/notifications.js +58 -48
  69. package/tools/observe.js +12 -15
  70. package/tools/onboarding.js +8 -11
  71. package/tools/open.js +13 -144
  72. package/tools/party-game.js +23 -12
  73. package/tools/patterns.js +2 -1
  74. package/tools/ping.js +5 -7
  75. package/tools/react.js +28 -30
  76. package/tools/{_deprecated/recall.js → recall.js} +5 -10
  77. package/tools/release.js +4 -2
  78. package/tools/{_deprecated/remember.js → remember.js} +4 -6
  79. package/tools/report.js +2 -2
  80. package/tools/request.js +6 -26
  81. package/tools/reserve.js +1 -1
  82. package/tools/session-fork.js +97 -0
  83. package/tools/session-save.js +109 -0
  84. package/tools/settings.js +30 -99
  85. package/tools/ship.js +74 -56
  86. package/tools/{_deprecated/skills-exchange.js → skills-exchange.js} +38 -39
  87. package/tools/social-inbox.js +22 -28
  88. package/tools/social-post.js +24 -27
  89. package/tools/solo-game.js +54 -46
  90. package/tools/start.js +14 -148
  91. package/tools/status.js +21 -68
  92. package/tools/submit.js +4 -2
  93. package/tools/suggest-tags.js +36 -33
  94. package/tools/summarize.js +19 -16
  95. package/tools/tag-suggestions.js +72 -73
  96. package/tools/test.js +1 -1
  97. package/tools/{_deprecated/tictactoe.js → tictactoe.js} +26 -26
  98. package/tools/token.js +4 -4
  99. package/tools/update.js +1 -2
  100. package/tools/watch.js +132 -112
  101. package/tools/who.js +20 -40
  102. package/tools/{_deprecated/wordassociation.js → wordassociation.js} +23 -20
  103. package/tools/workshop-buddy.js +52 -53
  104. package/tools/x-mentions.js +0 -1
  105. package/tools/x-reply.js +0 -1
  106. package/twitter.js +14 -20
  107. package/version.json +8 -10
  108. package/webhook-runner.js +132 -0
  109. package/auth-store.js +0 -148
  110. package/bridges/bridge-monitor.js +0 -388
  111. package/bridges/discord-bot.js +0 -431
  112. package/bridges/farcaster.js +0 -299
  113. package/bridges/telegram.js +0 -261
  114. package/bridges/webhook-health.js +0 -420
  115. package/bridges/webhook-server.js +0 -437
  116. package/bridges/whatsapp.js +0 -441
  117. package/bridges/x-webhook.js +0 -423
  118. package/games/arcade.js +0 -406
  119. package/games/chess.js +0 -451
  120. package/games/colorguess.js +0 -343
  121. package/games/crossword-words.js +0 -171
  122. package/games/crossword.js +0 -461
  123. package/games/drawing.js +0 -347
  124. package/games/gameroulette.js +0 -300
  125. package/games/gamerouter.js +0 -336
  126. package/games/gamestatus.js +0 -337
  127. package/games/guessnumber.js +0 -209
  128. package/games/hangman.js +0 -279
  129. package/games/memory.js +0 -338
  130. package/games/multiplayer-tictactoe.js +0 -389
  131. package/games/pixelart.js +0 -399
  132. package/games/quickduel.js +0 -354
  133. package/games/riddle.js +0 -371
  134. package/games/rockpaperscissors.js +0 -291
  135. package/games/snake.js +0 -406
  136. package/games/storybuilder.js +0 -343
  137. package/games/tictactoe.js +0 -345
  138. package/games/twentyquestions.js +0 -286
  139. package/games/twotruths.js +0 -207
  140. package/games/werewolf.js +0 -508
  141. package/games/wordassociation.js +0 -247
  142. package/games/wordchain.js +0 -135
  143. package/intelligence/interests.js +0 -369
  144. package/setup.js +0 -480
  145. package/smart-inbox.js +0 -276
  146. package/tools/_deprecated/auto-suggest-connections.js +0 -304
  147. package/tools/_deprecated/bootstrap-skills.js +0 -231
  148. package/tools/_deprecated/bridge-dashboard.js +0 -342
  149. package/tools/_deprecated/bridge-health.js +0 -400
  150. package/tools/_deprecated/bridge-live.js +0 -384
  151. package/tools/_deprecated/bridges.js +0 -383
  152. package/tools/_deprecated/colorguess.js +0 -281
  153. package/tools/_deprecated/discover-insights.js +0 -379
  154. package/tools/_deprecated/discover-momentum.js +0 -256
  155. package/tools/_deprecated/discovery-analytics.js +0 -345
  156. package/tools/_deprecated/discovery-auto-suggest.js +0 -275
  157. package/tools/_deprecated/discovery-bootstrap.js +0 -267
  158. package/tools/_deprecated/discovery-daily.js +0 -375
  159. package/tools/_deprecated/discovery-dashboard.js +0 -385
  160. package/tools/_deprecated/discovery-digest.js +0 -314
  161. package/tools/_deprecated/discovery-hub.js +0 -357
  162. package/tools/_deprecated/discovery-insights.js +0 -384
  163. package/tools/_deprecated/discovery-momentum.js +0 -281
  164. package/tools/_deprecated/discovery-monitor.js +0 -319
  165. package/tools/_deprecated/discovery-proactive.js +0 -300
  166. package/tools/_deprecated/draw.js +0 -317
  167. package/tools/_deprecated/farcaster.js +0 -307
  168. package/tools/_deprecated/games-catalog.js +0 -376
  169. package/tools/_deprecated/games.js +0 -313
  170. package/tools/_deprecated/guessnumber.js +0 -194
  171. package/tools/_deprecated/hangman.js +0 -129
  172. package/tools/_deprecated/multiplayer-tictactoe.js +0 -303
  173. package/tools/_deprecated/riddle.js +0 -240
  174. package/tools/_deprecated/run-bootstrap.js +0 -69
  175. package/tools/_deprecated/skills-analytics.js +0 -349
  176. package/tools/_deprecated/skills-bootstrap.js +0 -301
  177. package/tools/_deprecated/skills-dashboard.js +0 -268
  178. package/tools/_deprecated/skills.js +0 -380
  179. package/tools/_deprecated/smart-intro.js +0 -353
  180. package/tools/_deprecated/storybuilder.js +0 -331
  181. package/tools/_deprecated/telegram-bot.js +0 -183
  182. package/tools/_deprecated/telegram-setup.js +0 -214
  183. package/tools/_deprecated/twentyquestions.js +0 -143
  184. package/tools/_shared.js +0 -234
  185. package/tools/_work-context.js +0 -338
  186. package/tools/_work-context.manual-test.js +0 -199
  187. package/tools/_work-context.test.js +0 -260
  188. package/tools/activity.js +0 -220
  189. package/tools/agent-treasury.js +0 -288
  190. package/tools/analytics.js +0 -191
  191. package/tools/approve.js +0 -197
  192. package/tools/arcade.js +0 -173
  193. package/tools/artifacts-price.js +0 -107
  194. package/tools/ask-expert.js +0 -160
  195. package/tools/available.js +0 -120
  196. package/tools/become-expert.js +0 -150
  197. package/tools/broadcast.js +0 -325
  198. package/tools/chat.js +0 -202
  199. package/tools/collaborative-drawing.js +0 -286
  200. package/tools/connection-status.js +0 -178
  201. package/tools/earnings.js +0 -126
  202. package/tools/friends.js +0 -207
  203. package/tools/genesis.js +0 -233
  204. package/tools/gig-browse.js +0 -206
  205. package/tools/gig-complete.js +0 -144
  206. package/tools/health.js +0 -87
  207. package/tools/leaderboard.js +0 -117
  208. package/tools/lib/git-apply.js +0 -206
  209. package/tools/lib/git-bundle.js +0 -407
  210. package/tools/mint.js +0 -377
  211. package/tools/plan.js +0 -225
  212. package/tools/profile.js +0 -219
  213. package/tools/proof-of-work.js +0 -144
  214. package/tools/pulse.js +0 -218
  215. package/tools/reply.js +0 -166
  216. package/tools/reputation.js +0 -175
  217. package/tools/schedule.js +0 -367
  218. package/tools/search-messages.js +0 -123
  219. package/tools/session.js +0 -467
  220. package/tools/session_price.js +0 -128
  221. package/tools/smart-check.js +0 -201
  222. package/tools/social-processor.js +0 -445
  223. package/tools/streak.js +0 -147
  224. package/tools/stuck.js +0 -297
  225. package/tools/subscribe.js +0 -148
  226. package/tools/subscriptions.js +0 -134
  227. package/tools/tip.js +0 -193
  228. package/tools/wallet.js +0 -269
  229. package/tools/webhook-test.js +0 -388
  230. package/tools/withdraw.js +0 -145
  231. package/tools/work-summary.js +0 -96
  232. package/tools/workshop.js +0 -327
  233. /package/tools/{l2-bridge.js → _experimental/l2-bridge.js} +0 -0
  234. /package/tools/{l2.js → _experimental/l2.js} +0 -0
  235. /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
  };