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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uv-suite",
3
- "version": "0.26.0",
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
- const humans = events.filter(needsHuman).length;
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, i) => {
654
- const ev = events[i];
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)
@@ -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('http');
8
- const fs = require('fs');
9
- const path = require('path');
10
- const crypto = require('crypto');
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, 'events.json');
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, 'utf-8'));
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('Access-Control-Allow-Origin', '*');
54
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
55
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
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 === 'OPTIONS') {
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 === 'POST' && req.url === '/events') {
64
- let body = '';
65
- req.on('data', chunk => body += chunk);
66
- req.on('end', () => {
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, { 'Content-Type': 'application/json' });
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 === 'GET' && req.url === '/stream') {
85
+ if (req.method === "GET" && req.url === "/stream") {
86
86
  res.writeHead(200, {
87
- 'Content-Type': 'text/event-stream',
88
- 'Cache-Control': 'no-cache',
89
- 'Connection': 'keep-alive',
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(`data: ${JSON.stringify({ type: 'init', events: events.slice(-100) })}\n\n`);
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 { res.write(': ping\n\n'); } catch (e) { clearInterval(keepAlive); }
101
+ try {
102
+ res.write(": ping\n\n");
103
+ } catch (e) {
104
+ clearInterval(keepAlive);
105
+ }
100
106
  }, 15000);
101
107
 
102
- req.on('close', () => {
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 === 'GET' && req.url.startsWith('/events')) {
111
- res.writeHead(200, { 'Content-Type': 'application/json' });
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 === 'GET' && (req.url === '/' || req.url === '/index.html')) {
118
- const html = fs.readFileSync(path.join(__dirname, 'dashboard.html'), 'utf-8');
119
- res.writeHead(200, { 'Content-Type': 'text/html' });
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('not found');
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(`Waiting for hook events on POST http://localhost:${PORT}/events`);
176
+ console.log(
177
+ `Waiting for hook events on POST http://localhost:${PORT}/events`,
178
+ );
132
179
  });