slashvibe-mcp 0.3.19 → 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 +14 -1
- package/index.js +19 -7
- package/package.json +2 -1
- package/smart-inbox.js +276 -0
- package/store/api.js +100 -4
- package/tools/broadcast.js +45 -6
- package/tools/health.js +87 -0
- package/tools/leaderboard.js +117 -0
- package/tools/lib/git-apply.js +206 -0
- package/tools/lib/git-bundle.js +407 -0
- package/tools/session.js +50 -3
- package/tools/streak.js +147 -0
package/README.md
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
# vibe-mcp
|
|
2
2
|
|
|
3
|
-
Social layer for
|
|
3
|
+
Social layer for AI-assisted coding. DMs, presence, and connection between developers.
|
|
4
|
+
|
|
5
|
+
**Works in:** Claude Code • Cursor • Any MCP-compatible IDE
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
**Claude Code:**
|
|
10
|
+
```bash
|
|
11
|
+
npx slashvibe-mcp setup
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
**Cursor:** See [Cursor Setup Guide](docs/CURSOR_SETUP.md)
|
|
15
|
+
|
|
16
|
+
**Other IDEs:** Any editor supporting MCP protocol can use the manual config below.
|
|
4
17
|
|
|
5
18
|
## Installation
|
|
6
19
|
|
package/index.js
CHANGED
|
@@ -99,10 +99,11 @@ async function getPresenceFooter() {
|
|
|
99
99
|
const handle = config.getHandle();
|
|
100
100
|
if (!handle) return '';
|
|
101
101
|
|
|
102
|
-
// Fetch presence and
|
|
103
|
-
const [users, unreadCount] = await Promise.all([
|
|
102
|
+
// Fetch presence, unread, and live broadcasts in parallel
|
|
103
|
+
const [users, unreadCount, liveCount] = await Promise.all([
|
|
104
104
|
store.getActiveUsers().catch(() => []),
|
|
105
|
-
store.getUnreadCount(handle).catch(() => 0)
|
|
105
|
+
store.getUnreadCount(handle).catch(() => 0),
|
|
106
|
+
store.getLiveBroadcastCount().catch(() => 0)
|
|
106
107
|
]);
|
|
107
108
|
|
|
108
109
|
// Filter out self
|
|
@@ -125,7 +126,7 @@ async function getPresenceFooter() {
|
|
|
125
126
|
// Build the visible footer
|
|
126
127
|
let footer = '\n\n────────────────────────────────────────\n';
|
|
127
128
|
|
|
128
|
-
// Line 1: vibe · X online · Y unread
|
|
129
|
+
// Line 1: vibe · X online · Y unread · Z live
|
|
129
130
|
const parts = ['vibe'];
|
|
130
131
|
if (onlineCount > 0) {
|
|
131
132
|
parts.push(`${onlineCount} online`);
|
|
@@ -133,6 +134,9 @@ async function getPresenceFooter() {
|
|
|
133
134
|
if (unreadCount > 0) {
|
|
134
135
|
parts.push(`**${unreadCount} unread**`);
|
|
135
136
|
}
|
|
137
|
+
if (liveCount > 0) {
|
|
138
|
+
parts.push(`🔴 ${liveCount} live`);
|
|
139
|
+
}
|
|
136
140
|
footer += parts.join(' · ');
|
|
137
141
|
|
|
138
142
|
// Line 2: Activity hints (if anyone is online)
|
|
@@ -154,8 +158,10 @@ async function getPresenceFooter() {
|
|
|
154
158
|
}
|
|
155
159
|
});
|
|
156
160
|
footer += hints.join(' · ');
|
|
157
|
-
} else if (unreadCount === 0) {
|
|
158
|
-
footer += '\n_room is
|
|
161
|
+
} else if (unreadCount === 0 && liveCount === 0) {
|
|
162
|
+
footer += '\n_room is quiet · `vibe broadcast start` to go live_';
|
|
163
|
+
} else if (unreadCount === 0 && liveCount > 0) {
|
|
164
|
+
footer += '\n_no messages · watch live broadcasts with `vibe watch`_';
|
|
159
165
|
}
|
|
160
166
|
|
|
161
167
|
footer += '\n────────────────────────────────────────';
|
|
@@ -281,7 +287,13 @@ const coreTools = {
|
|
|
281
287
|
vibe_withdraw: require('./tools/withdraw'),
|
|
282
288
|
vibe_session_price: require('./tools/session_price'),
|
|
283
289
|
vibe_subscribe: require('./tools/subscribe'),
|
|
284
|
-
vibe_subscriptions: require('./tools/subscriptions')
|
|
290
|
+
vibe_subscriptions: require('./tools/subscriptions'),
|
|
291
|
+
// NFT Minting — Mint artifacts to VIBE L2, Base, or Ethereum
|
|
292
|
+
vibe_mint: require('./tools/mint'),
|
|
293
|
+
// Platform Status — Health, leaderboard, streaks
|
|
294
|
+
vibe_health: require('./tools/health'),
|
|
295
|
+
vibe_leaderboard: require('./tools/leaderboard'),
|
|
296
|
+
vibe_streak: require('./tools/streak')
|
|
285
297
|
};
|
|
286
298
|
|
|
287
299
|
// Admin tools (only loaded when VIBE_ADMIN=true)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slashvibe-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.21",
|
|
4
4
|
"mcpName": "io.github.vibecodinginc/vibe",
|
|
5
5
|
"description": "Social layer for Claude Code - DMs, presence, and connection between AI-assisted developers",
|
|
6
6
|
"main": "index.js",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"notify.js",
|
|
50
50
|
"notification-emitter.js",
|
|
51
51
|
"analytics.js",
|
|
52
|
+
"smart-inbox.js",
|
|
52
53
|
"presence.js",
|
|
53
54
|
"prompts.js",
|
|
54
55
|
"twitter.js",
|
package/smart-inbox.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Inbox Checking
|
|
3
|
+
*
|
|
4
|
+
* Intelligent notification checking that triggers at natural breaks:
|
|
5
|
+
* - After git commits
|
|
6
|
+
* - After test/build completion
|
|
7
|
+
* - After periods of activity (not idle)
|
|
8
|
+
* - On explicit check request
|
|
9
|
+
*
|
|
10
|
+
* Uses debouncing to avoid notification spam.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const config = require('./config');
|
|
14
|
+
const notify = require('./notify');
|
|
15
|
+
const store = require('./store');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
// State file for tracking check timing
|
|
20
|
+
const SMART_STATE_FILE = path.join(config.VIBE_DIR, '.smart_inbox_state.json');
|
|
21
|
+
|
|
22
|
+
// Timing constants
|
|
23
|
+
const MIN_CHECK_INTERVAL = 60 * 1000; // Minimum 1 minute between checks
|
|
24
|
+
const ACTIVITY_COOLDOWN = 5 * 60 * 1000; // 5 min after activity burst
|
|
25
|
+
const COMMIT_DELAY = 3 * 1000; // 3 second delay after commit (let it settle)
|
|
26
|
+
const TEST_DELAY = 2 * 1000; // 2 second delay after test
|
|
27
|
+
const BUILD_DELAY = 5 * 1000; // 5 second delay after build
|
|
28
|
+
|
|
29
|
+
// Trigger types
|
|
30
|
+
const TRIGGERS = {
|
|
31
|
+
COMMIT: 'commit',
|
|
32
|
+
TEST_PASS: 'test_pass',
|
|
33
|
+
TEST_FAIL: 'test_fail',
|
|
34
|
+
BUILD_COMPLETE: 'build_complete',
|
|
35
|
+
BUILD_FAIL: 'build_fail',
|
|
36
|
+
NATURAL_BREAK: 'natural_break',
|
|
37
|
+
EXPLICIT: 'explicit',
|
|
38
|
+
SESSION_START: 'session_start',
|
|
39
|
+
SESSION_END: 'session_end',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function loadState() {
|
|
43
|
+
try {
|
|
44
|
+
if (fs.existsSync(SMART_STATE_FILE)) {
|
|
45
|
+
return JSON.parse(fs.readFileSync(SMART_STATE_FILE, 'utf8'));
|
|
46
|
+
}
|
|
47
|
+
} catch (e) {}
|
|
48
|
+
return {
|
|
49
|
+
lastCheck: null,
|
|
50
|
+
lastTrigger: null,
|
|
51
|
+
checkCount: 0,
|
|
52
|
+
triggersToday: {},
|
|
53
|
+
activityBurst: null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function saveState(state) {
|
|
58
|
+
try {
|
|
59
|
+
fs.writeFileSync(SMART_STATE_FILE, JSON.stringify(state, null, 2));
|
|
60
|
+
} catch (e) {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check if we should actually run a check based on timing
|
|
65
|
+
*/
|
|
66
|
+
function shouldCheck(trigger) {
|
|
67
|
+
const state = loadState();
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
|
|
70
|
+
// Always allow explicit checks
|
|
71
|
+
if (trigger === TRIGGERS.EXPLICIT) {
|
|
72
|
+
return { should: true, reason: 'explicit_request' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check minimum interval
|
|
76
|
+
if (state.lastCheck && (now - state.lastCheck) < MIN_CHECK_INTERVAL) {
|
|
77
|
+
return { should: false, reason: 'too_soon', wait: MIN_CHECK_INTERVAL - (now - state.lastCheck) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Session events always trigger
|
|
81
|
+
if (trigger === TRIGGERS.SESSION_START || trigger === TRIGGERS.SESSION_END) {
|
|
82
|
+
return { should: true, reason: 'session_event' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// For build/test/commit, we want them but with debounce
|
|
86
|
+
if ([TRIGGERS.COMMIT, TRIGGERS.TEST_PASS, TRIGGERS.TEST_FAIL,
|
|
87
|
+
TRIGGERS.BUILD_COMPLETE, TRIGGERS.BUILD_FAIL].includes(trigger)) {
|
|
88
|
+
// If same trigger happened in last 30 seconds, skip
|
|
89
|
+
if (state.lastTrigger === trigger && state.lastCheck && (now - state.lastCheck) < 30000) {
|
|
90
|
+
return { should: false, reason: 'debounced', wait: 30000 - (now - state.lastCheck) };
|
|
91
|
+
}
|
|
92
|
+
return { should: true, reason: 'event_trigger' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Natural breaks - check if there's been activity recently
|
|
96
|
+
if (trigger === TRIGGERS.NATURAL_BREAK) {
|
|
97
|
+
// If we haven't checked in 5+ minutes and there's been activity, good time
|
|
98
|
+
if (!state.lastCheck || (now - state.lastCheck) > ACTIVITY_COOLDOWN) {
|
|
99
|
+
return { should: true, reason: 'natural_break' };
|
|
100
|
+
}
|
|
101
|
+
return { should: false, reason: 'not_time_yet' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { should: true, reason: 'default' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Perform smart inbox check
|
|
109
|
+
* Returns notification info if any
|
|
110
|
+
*/
|
|
111
|
+
async function check(trigger = TRIGGERS.EXPLICIT, context = {}) {
|
|
112
|
+
const decision = shouldCheck(trigger);
|
|
113
|
+
|
|
114
|
+
if (!decision.should) {
|
|
115
|
+
return {
|
|
116
|
+
checked: false,
|
|
117
|
+
reason: decision.reason,
|
|
118
|
+
waitMs: decision.wait || 0,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const handle = config.getHandle();
|
|
123
|
+
if (!handle) {
|
|
124
|
+
return { checked: false, reason: 'not_initialized' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const state = loadState();
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
|
|
130
|
+
// Apply appropriate delay based on trigger
|
|
131
|
+
let delay = 0;
|
|
132
|
+
switch (trigger) {
|
|
133
|
+
case TRIGGERS.COMMIT:
|
|
134
|
+
delay = COMMIT_DELAY;
|
|
135
|
+
break;
|
|
136
|
+
case TRIGGERS.TEST_PASS:
|
|
137
|
+
case TRIGGERS.TEST_FAIL:
|
|
138
|
+
delay = TEST_DELAY;
|
|
139
|
+
break;
|
|
140
|
+
case TRIGGERS.BUILD_COMPLETE:
|
|
141
|
+
case TRIGGERS.BUILD_FAIL:
|
|
142
|
+
delay = BUILD_DELAY;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (delay > 0) {
|
|
147
|
+
await new Promise(r => setTimeout(r, delay));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Run the actual notification check
|
|
151
|
+
let unreadCount = 0;
|
|
152
|
+
let notified = false;
|
|
153
|
+
let newUsers = [];
|
|
154
|
+
let newShips = [];
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
// Get unread messages
|
|
158
|
+
const inbox = await store.getRawInbox(handle).catch(() => []);
|
|
159
|
+
unreadCount = inbox.length;
|
|
160
|
+
|
|
161
|
+
if (inbox.length > 0) {
|
|
162
|
+
notified = await notify.checkAndNotify(inbox);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Get presence for online users
|
|
166
|
+
const users = await store.getActiveUsers().catch(() => []);
|
|
167
|
+
if (users.length > 0) {
|
|
168
|
+
newUsers = notify.checkPresence(users);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Get ships from connections
|
|
172
|
+
try {
|
|
173
|
+
const localStore = require('./store/local');
|
|
174
|
+
const memories = localStore.getAllThreadMemories ? localStore.getAllThreadMemories() : {};
|
|
175
|
+
const memoryHandles = Object.keys(memories).map(h => h.toLowerCase());
|
|
176
|
+
if (memoryHandles.length > 0) {
|
|
177
|
+
newShips = await notify.checkShips(memoryHandles);
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {
|
|
180
|
+
// Memory check is optional
|
|
181
|
+
}
|
|
182
|
+
} catch (e) {
|
|
183
|
+
// Silent fail
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Update state
|
|
187
|
+
state.lastCheck = now;
|
|
188
|
+
state.lastTrigger = trigger;
|
|
189
|
+
state.checkCount++;
|
|
190
|
+
|
|
191
|
+
// Track triggers by day
|
|
192
|
+
const today = new Date().toISOString().split('T')[0];
|
|
193
|
+
if (!state.triggersToday || state.triggersToday.date !== today) {
|
|
194
|
+
state.triggersToday = { date: today, counts: {} };
|
|
195
|
+
}
|
|
196
|
+
state.triggersToday.counts[trigger] = (state.triggersToday.counts[trigger] || 0) + 1;
|
|
197
|
+
|
|
198
|
+
saveState(state);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
checked: true,
|
|
202
|
+
trigger,
|
|
203
|
+
reason: decision.reason,
|
|
204
|
+
unreadCount,
|
|
205
|
+
notified,
|
|
206
|
+
newOnline: newUsers.map(u => u.handle),
|
|
207
|
+
newShips: newShips.map(s => s.author),
|
|
208
|
+
context,
|
|
209
|
+
timestamp: new Date().toISOString(),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Trigger after git commit
|
|
215
|
+
*/
|
|
216
|
+
async function afterCommit(commitMessage = '', branch = '') {
|
|
217
|
+
return check(TRIGGERS.COMMIT, { commitMessage, branch });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Trigger after test completion
|
|
222
|
+
*/
|
|
223
|
+
async function afterTest(passed = true, testOutput = '') {
|
|
224
|
+
const trigger = passed ? TRIGGERS.TEST_PASS : TRIGGERS.TEST_FAIL;
|
|
225
|
+
return check(trigger, { passed, testOutput: testOutput.slice(0, 200) });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Trigger after build completion
|
|
230
|
+
*/
|
|
231
|
+
async function afterBuild(success = true, buildOutput = '') {
|
|
232
|
+
const trigger = success ? TRIGGERS.BUILD_COMPLETE : TRIGGERS.BUILD_FAIL;
|
|
233
|
+
return check(trigger, { success, buildOutput: buildOutput.slice(0, 200) });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Trigger on natural break (between tasks, file saves, etc.)
|
|
238
|
+
*/
|
|
239
|
+
async function onNaturalBreak(activity = '') {
|
|
240
|
+
return check(TRIGGERS.NATURAL_BREAK, { activity });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Explicit check (user requested)
|
|
245
|
+
*/
|
|
246
|
+
async function checkNow() {
|
|
247
|
+
return check(TRIGGERS.EXPLICIT);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get check stats
|
|
252
|
+
*/
|
|
253
|
+
function getStats() {
|
|
254
|
+
const state = loadState();
|
|
255
|
+
const now = Date.now();
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
lastCheck: state.lastCheck ? new Date(state.lastCheck).toISOString() : null,
|
|
259
|
+
lastTrigger: state.lastTrigger,
|
|
260
|
+
checkCount: state.checkCount,
|
|
261
|
+
todayTriggers: state.triggersToday?.counts || {},
|
|
262
|
+
timeSinceLastCheck: state.lastCheck ? Math.floor((now - state.lastCheck) / 1000) : null,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
module.exports = {
|
|
267
|
+
TRIGGERS,
|
|
268
|
+
check,
|
|
269
|
+
afterCommit,
|
|
270
|
+
afterTest,
|
|
271
|
+
afterBuild,
|
|
272
|
+
onNaturalBreak,
|
|
273
|
+
checkNow,
|
|
274
|
+
getStats,
|
|
275
|
+
shouldCheck,
|
|
276
|
+
};
|
package/store/api.js
CHANGED
|
@@ -17,6 +17,30 @@ const API_URL = process.env.VIBE_API_URL || 'https://www.slashvibe.dev';
|
|
|
17
17
|
// Default timeout for API requests (10 seconds)
|
|
18
18
|
const REQUEST_TIMEOUT = 10000;
|
|
19
19
|
|
|
20
|
+
// Retry configuration
|
|
21
|
+
const MAX_RETRIES = 3;
|
|
22
|
+
const INITIAL_RETRY_DELAY = 500; // 500ms
|
|
23
|
+
const MAX_RETRY_DELAY = 5000; // 5 seconds
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Calculate exponential backoff delay with jitter
|
|
27
|
+
* @param {number} attempt - Current attempt number (0-based)
|
|
28
|
+
* @returns {number} Delay in milliseconds
|
|
29
|
+
*/
|
|
30
|
+
function getBackoffDelay(attempt) {
|
|
31
|
+
const exponentialDelay = INITIAL_RETRY_DELAY * Math.pow(2, attempt);
|
|
32
|
+
const jitter = Math.random() * 0.3 * exponentialDelay; // 0-30% jitter
|
|
33
|
+
return Math.min(exponentialDelay + jitter, MAX_RETRY_DELAY);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sleep for a given duration
|
|
38
|
+
* @param {number} ms - Milliseconds to sleep
|
|
39
|
+
*/
|
|
40
|
+
function sleep(ms) {
|
|
41
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
42
|
+
}
|
|
43
|
+
|
|
20
44
|
/**
|
|
21
45
|
* Force reload of config module to pick up auth changes
|
|
22
46
|
* This clears the require cache and reloads the config from disk
|
|
@@ -34,7 +58,10 @@ function reloadConfig() {
|
|
|
34
58
|
}
|
|
35
59
|
}
|
|
36
60
|
|
|
37
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Internal single request (no retries)
|
|
63
|
+
*/
|
|
64
|
+
function requestOnce(method, path, data = null, options = {}) {
|
|
38
65
|
return new Promise((resolve, reject) => {
|
|
39
66
|
const url = new URL(path, API_URL);
|
|
40
67
|
const isHttps = url.protocol === 'https:';
|
|
@@ -124,11 +151,11 @@ function request(method, path, data = null, options = {}) {
|
|
|
124
151
|
// Handle timeout
|
|
125
152
|
req.on('timeout', () => {
|
|
126
153
|
req.destroy();
|
|
127
|
-
resolve({ success: false, error: 'Request timeout', timeout: true });
|
|
154
|
+
resolve({ success: false, error: 'Request timeout', timeout: true, retryable: true });
|
|
128
155
|
});
|
|
129
156
|
|
|
130
157
|
req.on('error', (e) => {
|
|
131
|
-
resolve({ success: false, error: e.message, network: true });
|
|
158
|
+
resolve({ success: false, error: e.message, network: true, retryable: true });
|
|
132
159
|
});
|
|
133
160
|
|
|
134
161
|
if (data) {
|
|
@@ -138,6 +165,40 @@ function request(method, path, data = null, options = {}) {
|
|
|
138
165
|
});
|
|
139
166
|
}
|
|
140
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
|
+
|
|
141
202
|
// ============ PRESENCE ============
|
|
142
203
|
|
|
143
204
|
// Use v2 API by default (Postgres-backed)
|
|
@@ -318,7 +379,23 @@ async function getActiveUsers() {
|
|
|
318
379
|
}
|
|
319
380
|
|
|
320
381
|
async function setVisibility(handle, visible) {
|
|
321
|
-
|
|
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
|
+
}
|
|
322
399
|
}
|
|
323
400
|
|
|
324
401
|
// ============ MESSAGES ============
|
|
@@ -512,6 +589,22 @@ async function getUnreadCount(handle) {
|
|
|
512
589
|
}
|
|
513
590
|
}
|
|
514
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
|
+
|
|
515
608
|
// Get raw inbox messages (for notification checks)
|
|
516
609
|
// V2: Fetches threads and constructs message objects from unread threads
|
|
517
610
|
// Read state is synced via Postgres cursors, so if you read on iOS it's reflected here
|
|
@@ -1096,6 +1189,9 @@ module.exports = {
|
|
|
1096
1189
|
markMessagesDelivered,
|
|
1097
1190
|
searchMessages,
|
|
1098
1191
|
|
|
1192
|
+
// Watch Me Code
|
|
1193
|
+
getLiveBroadcastCount,
|
|
1194
|
+
|
|
1099
1195
|
// Consent
|
|
1100
1196
|
getConsentStatus,
|
|
1101
1197
|
getPendingConsents,
|
package/tools/broadcast.js
CHANGED
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Commands:
|
|
8
8
|
* - broadcast start "Building X" → Start streaming
|
|
9
|
-
* - broadcast stop → End + show stats
|
|
9
|
+
* - broadcast stop → End + show stats + git bundle
|
|
10
10
|
* - broadcast status → Check if broadcasting
|
|
11
11
|
* - broadcast stream <data> → Push terminal data (internal)
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
const config = require('../config');
|
|
15
|
+
const gitBundle = require('./lib/git-bundle');
|
|
15
16
|
|
|
16
17
|
const definition = {
|
|
17
18
|
name: 'vibe_broadcast',
|
|
@@ -74,12 +75,18 @@ async function handler(args) {
|
|
|
74
75
|
return { display: `⚠️ Failed to start broadcast: ${data.error}` };
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
// Capture git state at session start
|
|
79
|
+
const gitState = gitBundle.captureSessionStart();
|
|
80
|
+
|
|
77
81
|
// Store broadcast state
|
|
78
82
|
activeBroadcast = {
|
|
79
83
|
roomId: data.roomId,
|
|
80
84
|
shareUrl: data.shareUrl,
|
|
81
85
|
title,
|
|
82
|
-
startedAt: Date.now()
|
|
86
|
+
startedAt: Date.now(),
|
|
87
|
+
// Git tracking for bundle generation on stop
|
|
88
|
+
gitInitialCommit: gitState.success ? gitState.commit : null,
|
|
89
|
+
gitBranch: gitState.success ? gitState.branch : null,
|
|
83
90
|
};
|
|
84
91
|
|
|
85
92
|
// Also store in config for persistence across tool calls
|
|
@@ -109,12 +116,29 @@ async function handler(args) {
|
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
try {
|
|
119
|
+
// Prepare git bundle if we have a starting commit
|
|
120
|
+
let bundlePayload = null;
|
|
121
|
+
if (broadcast.gitInitialCommit) {
|
|
122
|
+
const bundleResult = gitBundle.createBundle(broadcast.gitInitialCommit);
|
|
123
|
+
if (bundleResult.success) {
|
|
124
|
+
bundlePayload = {
|
|
125
|
+
base64: bundleResult.buffer.toString('base64'),
|
|
126
|
+
metadata: bundleResult.metadata,
|
|
127
|
+
};
|
|
128
|
+
} else {
|
|
129
|
+
// Log but don't fail - bundle is optional
|
|
130
|
+
console.log(`[broadcast] Git bundle skipped: ${bundleResult.error}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
112
134
|
const response = await fetch(`${apiUrl}/api/watch`, {
|
|
113
135
|
method: 'POST',
|
|
114
136
|
headers: { 'Content-Type': 'application/json' },
|
|
115
137
|
body: JSON.stringify({
|
|
116
138
|
action: 'stop',
|
|
117
|
-
roomId: broadcast.roomId
|
|
139
|
+
roomId: broadcast.roomId,
|
|
140
|
+
// Include git bundle if available
|
|
141
|
+
gitBundle: bundlePayload,
|
|
118
142
|
})
|
|
119
143
|
});
|
|
120
144
|
|
|
@@ -136,10 +160,25 @@ async function handler(args) {
|
|
|
136
160
|
|
|
137
161
|
let display = `📺 Broadcast ended\n\n`;
|
|
138
162
|
display += `**Duration:** ${formatDuration(duration)}\n`;
|
|
139
|
-
display += `**Room:** ${broadcast.roomId}\n
|
|
163
|
+
display += `**Room:** ${broadcast.roomId}\n`;
|
|
164
|
+
|
|
165
|
+
// Show git bundle status
|
|
166
|
+
if (bundlePayload) {
|
|
167
|
+
const commits = bundlePayload.metadata.commitCount || 0;
|
|
168
|
+
const files = bundlePayload.metadata.changedFiles?.length || 0;
|
|
169
|
+
display += `**Git:** ${commits} commit${commits !== 1 ? 's' : ''}, ${files} file${files !== 1 ? 's' : ''} bundled 📦\n`;
|
|
170
|
+
} else if (broadcast.gitInitialCommit) {
|
|
171
|
+
display += `**Git:** No new commits to bundle\n`;
|
|
172
|
+
}
|
|
173
|
+
display += `\n`;
|
|
140
174
|
|
|
141
|
-
//
|
|
142
|
-
if (data.
|
|
175
|
+
// Show auto-save info
|
|
176
|
+
if (data.autoSave?.saved) {
|
|
177
|
+
display += `✅ Session auto-saved: \`${data.autoSave.sessionId}\`\n`;
|
|
178
|
+
if (bundlePayload) {
|
|
179
|
+
display += `_Code bundle included - viewers can fork the full code!_\n`;
|
|
180
|
+
}
|
|
181
|
+
} else if (data.suggestions) {
|
|
143
182
|
display += `_💡 Want to save this session? Use \`vibe session save\`_`;
|
|
144
183
|
} else {
|
|
145
184
|
display += `_💡 Save this broadcast as a replayable session:_\n`;
|