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.
- package/LICENSE +21 -0
- package/README.md +280 -47
- package/auto-update.js +10 -15
- package/config.js +36 -31
- package/crypto.js +1 -6
- package/debug.js +12 -0
- package/discord.js +19 -19
- package/eslint.config.js +54 -0
- package/index.js +217 -207
- package/intelligence/index.js +2 -9
- package/intelligence/infer.js +10 -16
- package/intelligence/patterns.js +23 -18
- package/intelligence/proactive.js +16 -15
- package/intelligence/serendipity.js +57 -20
- package/memory.js +13 -8
- package/migrate-v2.js +72 -0
- package/notification-emitter.js +2 -2
- package/notify.js +39 -14
- package/package.json +28 -29
- package/post-install.js +141 -0
- package/presence.js +2 -2
- package/prompts.js +5 -9
- package/protocol/index.js +123 -87
- package/protocol/telegram-commands.js +36 -37
- package/store/api.js +358 -529
- package/store/local.js +9 -10
- package/store/profiles.js +48 -192
- package/store/reservations.js +2 -9
- package/store/skills.js +69 -71
- package/store/sqlite.js +355 -0
- package/test-skills-bootstrap.js +20 -0
- package/test-v2-integration.js +385 -0
- package/tools/_actions.js +48 -387
- package/tools/_connection-queue.js +45 -56
- package/tools/_discovery-enhanced.js +52 -57
- package/tools/_discovery.js +87 -185
- package/tools/{l2-status.js → _experimental/l2-status.js} +68 -70
- package/tools/{shipback.js → _experimental/shipback.js} +4 -3
- package/tools/_proactive-discovery.js +60 -73
- package/tools/_shared/index.js +41 -64
- package/tools/admin-inbox.js +10 -15
- package/tools/agents.js +1 -1
- package/tools/artifact-create.js +13 -23
- package/tools/artifact-view.js +4 -4
- package/tools/{_deprecated/back.js → back.js} +1 -1
- package/tools/bye.js +3 -5
- package/tools/consent.js +2 -2
- package/tools/context.js +9 -10
- package/tools/crossword.js +3 -2
- package/tools/discover.js +94 -356
- package/tools/dm.js +27 -86
- package/tools/doctor.js +12 -41
- package/tools/drawing.js +34 -20
- package/tools/echo.js +11 -11
- package/tools/feed.js +30 -58
- package/tools/follow.js +64 -187
- package/tools/{_deprecated/forget.js → forget.js} +4 -7
- package/tools/game.js +144 -48
- package/tools/handoff.js +6 -8
- package/tools/help.js +3 -3
- package/tools/idea.js +15 -27
- package/tools/inbox.js +121 -293
- package/tools/init.js +54 -151
- package/tools/invite.js +8 -21
- package/tools/migrate.js +27 -24
- package/tools/multiplayer-game.js +50 -40
- package/tools/{_deprecated/mute.js → mute.js} +4 -3
- package/tools/notifications.js +58 -48
- package/tools/observe.js +12 -15
- package/tools/onboarding.js +8 -11
- package/tools/open.js +13 -144
- package/tools/party-game.js +23 -12
- package/tools/patterns.js +2 -1
- package/tools/ping.js +5 -7
- package/tools/react.js +28 -30
- package/tools/{_deprecated/recall.js → recall.js} +5 -10
- package/tools/release.js +4 -2
- package/tools/{_deprecated/remember.js → remember.js} +4 -6
- package/tools/report.js +2 -2
- package/tools/request.js +6 -26
- package/tools/reserve.js +1 -1
- package/tools/session-fork.js +97 -0
- package/tools/session-save.js +109 -0
- package/tools/settings.js +30 -99
- package/tools/ship.js +74 -56
- package/tools/{_deprecated/skills-exchange.js → skills-exchange.js} +38 -39
- package/tools/social-inbox.js +22 -28
- package/tools/social-post.js +24 -27
- package/tools/solo-game.js +54 -46
- package/tools/start.js +14 -148
- package/tools/status.js +21 -68
- package/tools/submit.js +4 -2
- package/tools/suggest-tags.js +36 -33
- package/tools/summarize.js +19 -16
- package/tools/tag-suggestions.js +72 -73
- package/tools/test.js +1 -1
- package/tools/{_deprecated/tictactoe.js → tictactoe.js} +26 -26
- package/tools/token.js +4 -4
- package/tools/update.js +1 -2
- package/tools/watch.js +132 -112
- package/tools/who.js +20 -40
- package/tools/{_deprecated/wordassociation.js → wordassociation.js} +23 -20
- package/tools/workshop-buddy.js +52 -53
- package/tools/x-mentions.js +0 -1
- package/tools/x-reply.js +0 -1
- package/twitter.js +14 -20
- package/version.json +8 -10
- package/webhook-runner.js +132 -0
- package/auth-store.js +0 -148
- package/bridges/bridge-monitor.js +0 -388
- package/bridges/discord-bot.js +0 -431
- package/bridges/farcaster.js +0 -299
- package/bridges/telegram.js +0 -261
- package/bridges/webhook-health.js +0 -420
- package/bridges/webhook-server.js +0 -437
- package/bridges/whatsapp.js +0 -441
- package/bridges/x-webhook.js +0 -423
- package/games/arcade.js +0 -406
- package/games/chess.js +0 -451
- package/games/colorguess.js +0 -343
- package/games/crossword-words.js +0 -171
- package/games/crossword.js +0 -461
- package/games/drawing.js +0 -347
- package/games/gameroulette.js +0 -300
- package/games/gamerouter.js +0 -336
- package/games/gamestatus.js +0 -337
- package/games/guessnumber.js +0 -209
- package/games/hangman.js +0 -279
- package/games/memory.js +0 -338
- package/games/multiplayer-tictactoe.js +0 -389
- package/games/pixelart.js +0 -399
- package/games/quickduel.js +0 -354
- package/games/riddle.js +0 -371
- package/games/rockpaperscissors.js +0 -291
- package/games/snake.js +0 -406
- package/games/storybuilder.js +0 -343
- package/games/tictactoe.js +0 -345
- package/games/twentyquestions.js +0 -286
- package/games/twotruths.js +0 -207
- package/games/werewolf.js +0 -508
- package/games/wordassociation.js +0 -247
- package/games/wordchain.js +0 -135
- package/intelligence/interests.js +0 -369
- package/setup.js +0 -480
- package/smart-inbox.js +0 -276
- package/tools/_deprecated/auto-suggest-connections.js +0 -304
- package/tools/_deprecated/bootstrap-skills.js +0 -231
- package/tools/_deprecated/bridge-dashboard.js +0 -342
- package/tools/_deprecated/bridge-health.js +0 -400
- package/tools/_deprecated/bridge-live.js +0 -384
- package/tools/_deprecated/bridges.js +0 -383
- package/tools/_deprecated/colorguess.js +0 -281
- package/tools/_deprecated/discover-insights.js +0 -379
- package/tools/_deprecated/discover-momentum.js +0 -256
- package/tools/_deprecated/discovery-analytics.js +0 -345
- package/tools/_deprecated/discovery-auto-suggest.js +0 -275
- package/tools/_deprecated/discovery-bootstrap.js +0 -267
- package/tools/_deprecated/discovery-daily.js +0 -375
- package/tools/_deprecated/discovery-dashboard.js +0 -385
- package/tools/_deprecated/discovery-digest.js +0 -314
- package/tools/_deprecated/discovery-hub.js +0 -357
- package/tools/_deprecated/discovery-insights.js +0 -384
- package/tools/_deprecated/discovery-momentum.js +0 -281
- package/tools/_deprecated/discovery-monitor.js +0 -319
- package/tools/_deprecated/discovery-proactive.js +0 -300
- package/tools/_deprecated/draw.js +0 -317
- package/tools/_deprecated/farcaster.js +0 -307
- package/tools/_deprecated/games-catalog.js +0 -376
- package/tools/_deprecated/games.js +0 -313
- package/tools/_deprecated/guessnumber.js +0 -194
- package/tools/_deprecated/hangman.js +0 -129
- package/tools/_deprecated/multiplayer-tictactoe.js +0 -303
- package/tools/_deprecated/riddle.js +0 -240
- package/tools/_deprecated/run-bootstrap.js +0 -69
- package/tools/_deprecated/skills-analytics.js +0 -349
- package/tools/_deprecated/skills-bootstrap.js +0 -301
- package/tools/_deprecated/skills-dashboard.js +0 -268
- package/tools/_deprecated/skills.js +0 -380
- package/tools/_deprecated/smart-intro.js +0 -353
- package/tools/_deprecated/storybuilder.js +0 -331
- package/tools/_deprecated/telegram-bot.js +0 -183
- package/tools/_deprecated/telegram-setup.js +0 -214
- package/tools/_deprecated/twentyquestions.js +0 -143
- package/tools/_shared.js +0 -234
- package/tools/_work-context.js +0 -338
- package/tools/_work-context.manual-test.js +0 -199
- package/tools/_work-context.test.js +0 -260
- package/tools/activity.js +0 -220
- package/tools/agent-treasury.js +0 -288
- package/tools/analytics.js +0 -191
- package/tools/approve.js +0 -197
- package/tools/arcade.js +0 -173
- package/tools/artifacts-price.js +0 -107
- package/tools/ask-expert.js +0 -160
- package/tools/available.js +0 -120
- package/tools/become-expert.js +0 -150
- package/tools/broadcast.js +0 -325
- package/tools/chat.js +0 -202
- package/tools/collaborative-drawing.js +0 -286
- package/tools/connection-status.js +0 -178
- package/tools/earnings.js +0 -126
- package/tools/friends.js +0 -207
- package/tools/genesis.js +0 -233
- package/tools/gig-browse.js +0 -206
- package/tools/gig-complete.js +0 -144
- package/tools/health.js +0 -87
- package/tools/leaderboard.js +0 -117
- package/tools/lib/git-apply.js +0 -206
- package/tools/lib/git-bundle.js +0 -407
- package/tools/mint.js +0 -377
- package/tools/plan.js +0 -225
- package/tools/profile.js +0 -219
- package/tools/proof-of-work.js +0 -144
- package/tools/pulse.js +0 -218
- package/tools/reply.js +0 -166
- package/tools/reputation.js +0 -175
- package/tools/schedule.js +0 -367
- package/tools/search-messages.js +0 -123
- package/tools/session.js +0 -467
- package/tools/session_price.js +0 -128
- package/tools/smart-check.js +0 -201
- package/tools/social-processor.js +0 -445
- package/tools/streak.js +0 -147
- package/tools/stuck.js +0 -297
- package/tools/subscribe.js +0 -148
- package/tools/subscriptions.js +0 -134
- package/tools/tip.js +0 -193
- package/tools/wallet.js +0 -269
- package/tools/webhook-test.js +0 -388
- package/tools/withdraw.js +0 -145
- package/tools/work-summary.js +0 -96
- package/tools/workshop.js +0 -327
- /package/tools/{l2-bridge.js → _experimental/l2-bridge.js} +0 -0
- /package/tools/{l2.js → _experimental/l2.js} +0 -0
- /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
|
|
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
|
-
|
|
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
|
|
77
|
-
|
|
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,
|
|
47
|
+
const req = client.request(reqOptions, res => {
|
|
93
48
|
let body = '';
|
|
94
|
-
res.on('data', chunk => body += chunk);
|
|
95
|
-
res.on('end',
|
|
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
|
|
73
|
+
resolve({ success: false, error: 'Request timeout', timeout: true });
|
|
155
74
|
});
|
|
156
75
|
|
|
157
|
-
req.on('error',
|
|
158
|
-
resolve({ success: false, error: e.message, network: true
|
|
76
|
+
req.on('error', e => {
|
|
77
|
+
resolve({ success: false, error: e.message, network: true });
|
|
159
78
|
});
|
|
160
79
|
|
|
161
80
|
if (data) {
|
|
162
|
-
|
|
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 });
|
|
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.
|
|
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 @
|
|
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 });
|
|
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
|
-
|
|
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
|
|
163
|
+
// Add context (mood, file, etc.) if provided
|
|
281
164
|
if (context) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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',
|
|
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
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
192
|
+
console.error('Typing indicator failed:', e.message);
|
|
311
193
|
}
|
|
312
194
|
}
|
|
313
195
|
|
|
314
196
|
async function getTypingUsers(forHandle) {
|
|
315
197
|
try {
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
340
|
-
mood: u.mood || u.
|
|
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.
|
|
346
|
-
branch: u.
|
|
347
|
-
repo: u.
|
|
348
|
-
error: u.
|
|
349
|
-
note: u.
|
|
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.
|
|
352
|
-
awayAt: u.
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
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
|
-
|
|
404
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
537
|
-
|
|
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
|
-
//
|
|
543
|
-
if (
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
//
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
|
570
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
|
|
684
|
-
|
|
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
|
-
//
|
|
499
|
+
// 2. Fetch from API (sync with backend)
|
|
688
500
|
const result = await request('GET', `/api/messages?user=${myHandle}&with=${theirHandle}`);
|
|
689
|
-
|
|
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,
|
|
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
|
-
|
|
704
|
-
// V2: Explicit mark read via PATCH
|
|
705
|
-
if (USE_V2_MESSAGES && lastMessageId) {
|
|
549
|
+
// Fallback to SQLite if API fails
|
|
706
550
|
try {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
|
|
732
|
-
|
|
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
|
|
893
|
-
* @param {string} token - JWT token
|
|
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
|
|
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
|
-
*
|
|
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
|
|
834
|
+
async function getFollowing(handle) {
|
|
1021
835
|
try {
|
|
1022
|
-
const result = await request('
|
|
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
|
|
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
|
|
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/
|
|
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
|
-
|
|
1230
|
-
|
|
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
|
-
|
|
1234
|
-
verifyPrivyToken: verifyAuthToken // Backwards compat alias
|
|
1063
|
+
verifyPrivyToken
|
|
1235
1064
|
};
|