uv-suite 0.17.0 → 0.18.0
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/package.json +1 -1
- package/watchtower/dashboard.html +16 -13
- package/watchtower/events.json +11 -0
- package/watchtower/server.js +43 -59
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uv-suite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Portable framework for AI-assisted software development. 10 agents, 9 skills, 5 hooks, 4 personas. Works with Claude Code, Cursor, and Codex.",
|
|
5
5
|
"author": "Utsav Anand",
|
|
6
6
|
"license": "MIT",
|
|
@@ -225,28 +225,31 @@ function toggleAutoScroll() {
|
|
|
225
225
|
document.getElementById('btnAutoScroll').classList.toggle('active', autoScroll);
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
// WebSocket
|
|
228
|
+
// Server-Sent Events connection (replaces WebSocket — simpler, auto-reconnects)
|
|
229
229
|
function connect() {
|
|
230
|
-
const
|
|
231
|
-
ws = new WebSocket(`${proto}//${location.host}/ws`);
|
|
230
|
+
const source = new EventSource('/stream');
|
|
232
231
|
|
|
233
|
-
|
|
232
|
+
source.onopen = () => {
|
|
234
233
|
statusDot.className = 'dot on';
|
|
235
234
|
statusText.textContent = 'Connected';
|
|
236
235
|
};
|
|
237
236
|
|
|
238
|
-
|
|
237
|
+
source.onerror = () => {
|
|
239
238
|
statusDot.className = 'dot off';
|
|
240
|
-
statusText.textContent = '
|
|
241
|
-
|
|
239
|
+
statusText.textContent = 'Reconnecting...';
|
|
240
|
+
// EventSource auto-reconnects — no manual retry needed
|
|
242
241
|
};
|
|
243
242
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
data.events
|
|
248
|
-
|
|
249
|
-
|
|
243
|
+
source.onmessage = (msg) => {
|
|
244
|
+
try {
|
|
245
|
+
const data = JSON.parse(msg.data);
|
|
246
|
+
if (data.type === 'init' && data.events) {
|
|
247
|
+
data.events.forEach(addEvent);
|
|
248
|
+
} else {
|
|
249
|
+
addEvent(data);
|
|
250
|
+
}
|
|
251
|
+
} catch (e) {
|
|
252
|
+
// ignore parse errors (ping frames, etc.)
|
|
250
253
|
}
|
|
251
254
|
};
|
|
252
255
|
}
|
package/watchtower/events.json
CHANGED
|
@@ -7,5 +7,16 @@
|
|
|
7
7
|
"cwd": "/tmp",
|
|
8
8
|
"_ts": 1776756371726,
|
|
9
9
|
"_id": "3f130976-6226-47ea-a477-18a16e194415"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"event_type": "PostToolUse",
|
|
13
|
+
"session_id": "test-session",
|
|
14
|
+
"source_app": "my-project",
|
|
15
|
+
"tool_name": "Edit",
|
|
16
|
+
"tool_input": {
|
|
17
|
+
"file_path": "src/app.ts"
|
|
18
|
+
},
|
|
19
|
+
"_ts": 1776757586012,
|
|
20
|
+
"_id": "efa9ae75-c0e0-48c2-a078-e7075ccef5a7"
|
|
10
21
|
}
|
|
11
22
|
]
|
package/watchtower/server.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// UV Suite Watchtower — lightweight observability server
|
|
4
|
-
// Zero dependencies beyond Node.js
|
|
4
|
+
// Zero dependencies beyond Node.js
|
|
5
|
+
// Uses Server-Sent Events (SSE) instead of WebSocket — simpler, auto-reconnects
|
|
5
6
|
|
|
6
7
|
const http = require('http');
|
|
7
8
|
const fs = require('fs');
|
|
8
9
|
const path = require('path');
|
|
10
|
+
const crypto = require('crypto');
|
|
9
11
|
|
|
10
12
|
const PORT = process.env.UVS_WATCHTOWER_PORT || 4200;
|
|
11
13
|
const DATA_FILE = path.join(__dirname, 'events.json');
|
|
@@ -21,64 +23,29 @@ try {
|
|
|
21
23
|
events = [];
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
//
|
|
25
|
-
const
|
|
26
|
+
// SSE clients
|
|
27
|
+
const sseClients = new Set();
|
|
26
28
|
|
|
27
29
|
function broadcast(event) {
|
|
28
|
-
const
|
|
29
|
-
for (const
|
|
30
|
-
try {
|
|
30
|
+
const data = JSON.stringify(event);
|
|
31
|
+
for (const res of sseClients) {
|
|
32
|
+
try {
|
|
33
|
+
res.write(`data: ${data}\n\n`);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
sseClients.delete(res);
|
|
36
|
+
}
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
function saveEvents() {
|
|
35
|
-
// Keep only the last MAX_EVENTS
|
|
36
41
|
if (events.length > MAX_EVENTS) {
|
|
37
42
|
events = events.slice(-MAX_EVENTS);
|
|
38
43
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
function upgradeToWebSocket(req, socket) {
|
|
46
|
-
const key = req.headers['sec-websocket-key'];
|
|
47
|
-
const accept = crypto.createHash('sha1')
|
|
48
|
-
.update(key + '258EAFA5-E914-47DA-95CA-5AB5DC085B11')
|
|
49
|
-
.digest('base64');
|
|
50
|
-
|
|
51
|
-
socket.write(
|
|
52
|
-
'HTTP/1.1 101 Switching Protocols\r\n' +
|
|
53
|
-
'Upgrade: websocket\r\n' +
|
|
54
|
-
'Connection: Upgrade\r\n' +
|
|
55
|
-
`Sec-WebSocket-Accept: ${accept}\r\n` +
|
|
56
|
-
'\r\n'
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
const ws = {
|
|
60
|
-
send(data) {
|
|
61
|
-
const buf = Buffer.from(data);
|
|
62
|
-
const frame = [];
|
|
63
|
-
frame.push(0x81); // text frame
|
|
64
|
-
if (buf.length < 126) {
|
|
65
|
-
frame.push(buf.length);
|
|
66
|
-
} else if (buf.length < 65536) {
|
|
67
|
-
frame.push(126, (buf.length >> 8) & 0xff, buf.length & 0xff);
|
|
68
|
-
} else {
|
|
69
|
-
frame.push(127);
|
|
70
|
-
for (let i = 7; i >= 0; i--) frame.push((buf.length >> (i * 8)) & 0xff);
|
|
71
|
-
}
|
|
72
|
-
socket.write(Buffer.concat([Buffer.from(frame), buf]));
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
clients.add(ws);
|
|
77
|
-
socket.on('close', () => clients.delete(ws));
|
|
78
|
-
socket.on('error', () => clients.delete(ws));
|
|
79
|
-
|
|
80
|
-
// Send recent events on connect
|
|
81
|
-
ws.send(JSON.stringify({ type: 'init', events: events.slice(-100) }));
|
|
44
|
+
try {
|
|
45
|
+
fs.writeFileSync(DATA_FILE, JSON.stringify(events, null, 2));
|
|
46
|
+
} catch (e) {
|
|
47
|
+
// ignore write errors
|
|
48
|
+
}
|
|
82
49
|
}
|
|
83
50
|
|
|
84
51
|
const server = http.createServer((req, res) => {
|
|
@@ -114,7 +81,32 @@ const server = http.createServer((req, res) => {
|
|
|
114
81
|
return;
|
|
115
82
|
}
|
|
116
83
|
|
|
117
|
-
// GET /
|
|
84
|
+
// GET /stream — SSE endpoint (replaces WebSocket)
|
|
85
|
+
if (req.method === 'GET' && req.url === '/stream') {
|
|
86
|
+
res.writeHead(200, {
|
|
87
|
+
'Content-Type': 'text/event-stream',
|
|
88
|
+
'Cache-Control': 'no-cache',
|
|
89
|
+
'Connection': 'keep-alive',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Send recent events as init
|
|
93
|
+
res.write(`data: ${JSON.stringify({ type: 'init', events: events.slice(-100) })}\n\n`);
|
|
94
|
+
|
|
95
|
+
sseClients.add(res);
|
|
96
|
+
|
|
97
|
+
// Keep-alive ping every 15 seconds
|
|
98
|
+
const keepAlive = setInterval(() => {
|
|
99
|
+
try { res.write(': ping\n\n'); } catch (e) { clearInterval(keepAlive); }
|
|
100
|
+
}, 15000);
|
|
101
|
+
|
|
102
|
+
req.on('close', () => {
|
|
103
|
+
sseClients.delete(res);
|
|
104
|
+
clearInterval(keepAlive);
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// GET /events — fetch recent events (REST fallback)
|
|
118
110
|
if (req.method === 'GET' && req.url.startsWith('/events')) {
|
|
119
111
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
120
112
|
res.end(JSON.stringify(events.slice(-100)));
|
|
@@ -133,14 +125,6 @@ const server = http.createServer((req, res) => {
|
|
|
133
125
|
res.end('not found');
|
|
134
126
|
});
|
|
135
127
|
|
|
136
|
-
server.on('upgrade', (req, socket, head) => {
|
|
137
|
-
if (req.url === '/ws') {
|
|
138
|
-
upgradeToWebSocket(req, socket);
|
|
139
|
-
} else {
|
|
140
|
-
socket.destroy();
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
|
|
144
128
|
server.listen(PORT, () => {
|
|
145
129
|
console.log(`UV Suite Watchtower running at http://localhost:${PORT}`);
|
|
146
130
|
console.log(`${events.length} events loaded from disk`);
|