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