uv-suite 0.26.0 → 0.26.1
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 +13 -3
- package/watchtower/server.js +76 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uv-suite",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.1",
|
|
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",
|
|
@@ -553,6 +553,8 @@ function renderEvent(ev) {
|
|
|
553
553
|
<span class="detail">${eventDetail(ev)}</span>
|
|
554
554
|
`;
|
|
555
555
|
|
|
556
|
+
div._ev = ev;
|
|
557
|
+
|
|
556
558
|
// Check filters
|
|
557
559
|
const typeMatch = !selectedType || type === selectedType;
|
|
558
560
|
const sessMatch = !selectedSession || sid === selectedSession;
|
|
@@ -607,7 +609,14 @@ function updateStats() {
|
|
|
607
609
|
const errors = events.filter(e => (e.event_type || e.hook_event_name || '').includes('Failure')).length;
|
|
608
610
|
document.getElementById('errorCount').textContent = errors;
|
|
609
611
|
document.getElementById('errorCount').className = 'n' + (errors > 0 ? ' alert' : '');
|
|
610
|
-
|
|
612
|
+
// Count sessions whose most recent event is waiting on a human — so the
|
|
613
|
+
// number drops back down once the session continues past the prompt.
|
|
614
|
+
const latestBySession = {};
|
|
615
|
+
for (const ev of events) {
|
|
616
|
+
const sid = ev.session_id || ev.source_app || 'unknown';
|
|
617
|
+
latestBySession[sid] = ev;
|
|
618
|
+
}
|
|
619
|
+
const humans = Object.values(latestBySession).filter(needsHuman).length;
|
|
611
620
|
document.getElementById('humanCount').textContent = humans;
|
|
612
621
|
document.getElementById('humanCount').className = 'n' + (humans > 0 ? ' alert' : '');
|
|
613
622
|
}
|
|
@@ -650,8 +659,9 @@ function refilter() {
|
|
|
650
659
|
selectedType = filterType.value;
|
|
651
660
|
selectedSession = filterSession.value;
|
|
652
661
|
const rows = timeline.querySelectorAll('.event');
|
|
653
|
-
rows.forEach((row
|
|
654
|
-
const ev =
|
|
662
|
+
rows.forEach((row) => {
|
|
663
|
+
const ev = row._ev;
|
|
664
|
+
if (!ev) return;
|
|
655
665
|
const sid = ev.session_id || ev.source_app || 'unknown';
|
|
656
666
|
const type = ev.event_type || ev.hook_event_name || '';
|
|
657
667
|
const show = (!selectedType || type === selectedType)
|
package/watchtower/server.js
CHANGED
|
@@ -4,20 +4,20 @@
|
|
|
4
4
|
// Zero dependencies beyond Node.js
|
|
5
5
|
// Uses Server-Sent Events (SSE) instead of WebSocket — simpler, auto-reconnects
|
|
6
6
|
|
|
7
|
-
const http = require(
|
|
8
|
-
const fs = require(
|
|
9
|
-
const path = require(
|
|
10
|
-
const crypto = require(
|
|
7
|
+
const http = require("http");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const crypto = require("crypto");
|
|
11
11
|
|
|
12
12
|
const PORT = process.env.UVS_WATCHTOWER_PORT || 4200;
|
|
13
|
-
const DATA_FILE = path.join(__dirname,
|
|
13
|
+
const DATA_FILE = path.join(__dirname, "events.json");
|
|
14
14
|
const MAX_EVENTS = 500;
|
|
15
15
|
|
|
16
16
|
// In-memory event store
|
|
17
17
|
let events = [];
|
|
18
18
|
try {
|
|
19
19
|
if (fs.existsSync(DATA_FILE)) {
|
|
20
|
-
events = JSON.parse(fs.readFileSync(DATA_FILE,
|
|
20
|
+
events = JSON.parse(fs.readFileSync(DATA_FILE, "utf-8"));
|
|
21
21
|
}
|
|
22
22
|
} catch (e) {
|
|
23
23
|
events = [];
|
|
@@ -50,20 +50,20 @@ function saveEvents() {
|
|
|
50
50
|
|
|
51
51
|
const server = http.createServer((req, res) => {
|
|
52
52
|
// CORS
|
|
53
|
-
res.setHeader(
|
|
54
|
-
res.setHeader(
|
|
55
|
-
res.setHeader(
|
|
53
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
54
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
55
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
56
56
|
|
|
57
|
-
if (req.method ===
|
|
57
|
+
if (req.method === "OPTIONS") {
|
|
58
58
|
res.writeHead(200);
|
|
59
59
|
return res.end();
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// POST /events — receive hook events
|
|
63
|
-
if (req.method ===
|
|
64
|
-
let body =
|
|
65
|
-
req.on(
|
|
66
|
-
req.on(
|
|
63
|
+
if (req.method === "POST" && req.url === "/events") {
|
|
64
|
+
let body = "";
|
|
65
|
+
req.on("data", (chunk) => (body += chunk));
|
|
66
|
+
req.on("end", () => {
|
|
67
67
|
try {
|
|
68
68
|
const event = JSON.parse(body);
|
|
69
69
|
event._ts = Date.now();
|
|
@@ -71,7 +71,7 @@ const server = http.createServer((req, res) => {
|
|
|
71
71
|
events.push(event);
|
|
72
72
|
broadcast(event);
|
|
73
73
|
saveEvents();
|
|
74
|
-
res.writeHead(200, {
|
|
74
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
75
75
|
res.end('{"ok":true}');
|
|
76
76
|
} catch (e) {
|
|
77
77
|
res.writeHead(400);
|
|
@@ -82,24 +82,30 @@ const server = http.createServer((req, res) => {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// GET /stream — SSE endpoint (replaces WebSocket)
|
|
85
|
-
if (req.method ===
|
|
85
|
+
if (req.method === "GET" && req.url === "/stream") {
|
|
86
86
|
res.writeHead(200, {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
"Content-Type": "text/event-stream",
|
|
88
|
+
"Cache-Control": "no-cache",
|
|
89
|
+
Connection: "keep-alive",
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
// Send recent events as init
|
|
93
|
-
res.write(
|
|
93
|
+
res.write(
|
|
94
|
+
`data: ${JSON.stringify({ type: "init", events: events.slice(-100) })}\n\n`,
|
|
95
|
+
);
|
|
94
96
|
|
|
95
97
|
sseClients.add(res);
|
|
96
98
|
|
|
97
99
|
// Keep-alive ping every 15 seconds
|
|
98
100
|
const keepAlive = setInterval(() => {
|
|
99
|
-
try {
|
|
101
|
+
try {
|
|
102
|
+
res.write(": ping\n\n");
|
|
103
|
+
} catch (e) {
|
|
104
|
+
clearInterval(keepAlive);
|
|
105
|
+
}
|
|
100
106
|
}, 15000);
|
|
101
107
|
|
|
102
|
-
req.on(
|
|
108
|
+
req.on("close", () => {
|
|
103
109
|
sseClients.delete(res);
|
|
104
110
|
clearInterval(keepAlive);
|
|
105
111
|
});
|
|
@@ -107,26 +113,67 @@ const server = http.createServer((req, res) => {
|
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
// GET /events — fetch recent events (REST fallback)
|
|
110
|
-
if (req.method ===
|
|
111
|
-
res.writeHead(200, {
|
|
116
|
+
if (req.method === "GET" && req.url.startsWith("/events")) {
|
|
117
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
112
118
|
res.end(JSON.stringify(events.slice(-100)));
|
|
113
119
|
return;
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
// GET / — serve dashboard
|
|
117
|
-
if (req.method ===
|
|
118
|
-
const html = fs.readFileSync(
|
|
119
|
-
|
|
123
|
+
if (req.method === "GET" && (req.url === "/" || req.url === "/index.html")) {
|
|
124
|
+
const html = fs.readFileSync(
|
|
125
|
+
path.join(__dirname, "dashboard.html"),
|
|
126
|
+
"utf-8",
|
|
127
|
+
);
|
|
128
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
120
129
|
res.end(html);
|
|
121
130
|
return;
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
res.writeHead(404);
|
|
125
|
-
res.end(
|
|
134
|
+
res.end("not found");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
server.on("error", (err) => {
|
|
138
|
+
if (err.code !== "EADDRINUSE") {
|
|
139
|
+
console.error("Watchtower server error:", err.message);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
// Port busy — probe to see if it's an existing watchtower or another process
|
|
143
|
+
const req = http.request(
|
|
144
|
+
{ host: "127.0.0.1", port: PORT, path: "/", method: "GET", timeout: 1500 },
|
|
145
|
+
(res) => {
|
|
146
|
+
let body = "";
|
|
147
|
+
res.on("data", (c) => (body += c));
|
|
148
|
+
res.on("end", () => {
|
|
149
|
+
if (/UV Suite Watchtower/.test(body)) {
|
|
150
|
+
console.log(
|
|
151
|
+
`UV Suite Watchtower is already running at http://localhost:${PORT}`,
|
|
152
|
+
);
|
|
153
|
+
process.exit(0);
|
|
154
|
+
} else {
|
|
155
|
+
console.error(`Port ${PORT} is in use by another process.`);
|
|
156
|
+
console.error(`Set UVS_WATCHTOWER_PORT to use a different port.`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
req.on("error", () => {
|
|
163
|
+
console.error(`Port ${PORT} is in use but not responding.`);
|
|
164
|
+
console.error(`Set UVS_WATCHTOWER_PORT to use a different port.`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
});
|
|
167
|
+
req.on("timeout", () => {
|
|
168
|
+
req.destroy();
|
|
169
|
+
});
|
|
170
|
+
req.end();
|
|
126
171
|
});
|
|
127
172
|
|
|
128
173
|
server.listen(PORT, () => {
|
|
129
174
|
console.log(`UV Suite Watchtower running at http://localhost:${PORT}`);
|
|
130
175
|
console.log(`${events.length} events loaded from disk`);
|
|
131
|
-
console.log(
|
|
176
|
+
console.log(
|
|
177
|
+
`Waiting for hook events on POST http://localhost:${PORT}/events`,
|
|
178
|
+
);
|
|
132
179
|
});
|