uv-suite 0.17.0 → 0.19.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/bin/cli.js CHANGED
@@ -86,10 +86,26 @@ function personaLabel(p) {
86
86
  return labels[p] || p;
87
87
  }
88
88
 
89
+ function ensureInstalled(persona) {
90
+ const settings = path.resolve('.claude/personas', `${persona}.json`);
91
+ if (!fs.existsSync(settings)) {
92
+ console.log('UV Suite not installed in this project. Installing...');
93
+ console.log('');
94
+ const installScript = path.join(UV_SUITE_DIR, 'install.sh');
95
+ try {
96
+ execSync(`bash "${installScript}" --persona ${persona}`, { stdio: 'inherit', timeout: 60000 });
97
+ } catch (e) {
98
+ // Install may timeout on pip installs but core files are already copied
99
+ }
100
+ console.log('');
101
+ }
102
+ }
103
+
89
104
  function launchClaude(persona, extra) {
105
+ ensureInstalled(persona);
90
106
  const settings = path.resolve('.claude/personas', `${persona}.json`);
91
107
  if (!fs.existsSync(settings)) {
92
- console.error(`Error: ${settings} not found. Run 'uv install' first.`);
108
+ console.error(`Error: installation failed. Run 'uvs install --persona ${persona}' manually.`);
93
109
  process.exit(1);
94
110
  }
95
111
  console.log(`UV Suite | Claude Code | ${personaLabel(persona)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uv-suite",
3
- "version": "0.17.0",
3
+ "version": "0.19.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 connection
228
+ // Server-Sent Events connection (replaces WebSocket — simpler, auto-reconnects)
229
229
  function connect() {
230
- const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
231
- ws = new WebSocket(`${proto}//${location.host}/ws`);
230
+ const source = new EventSource('/stream');
232
231
 
233
- ws.onopen = () => {
232
+ source.onopen = () => {
234
233
  statusDot.className = 'dot on';
235
234
  statusText.textContent = 'Connected';
236
235
  };
237
236
 
238
- ws.onclose = () => {
237
+ source.onerror = () => {
239
238
  statusDot.className = 'dot off';
240
- statusText.textContent = 'Disconnected — reconnecting...';
241
- setTimeout(connect, 2000);
239
+ statusText.textContent = 'Reconnecting...';
240
+ // EventSource auto-reconnects — no manual retry needed
242
241
  };
243
242
 
244
- ws.onmessage = (msg) => {
245
- const data = JSON.parse(msg.data);
246
- if (data.type === 'init' && data.events) {
247
- data.events.forEach(addEvent);
248
- } else {
249
- addEvent(data);
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
  }
@@ -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
  ]
@@ -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 (uses built-in http, fs, ws via raw upgrade)
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
- // WebSocket clients
25
- const clients = new Set();
26
+ // SSE clients
27
+ const sseClients = new Set();
26
28
 
27
29
  function broadcast(event) {
28
- const msg = JSON.stringify(event);
29
- for (const ws of clients) {
30
- try { ws.send(msg); } catch (e) { clients.delete(ws); }
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
- fs.writeFileSync(DATA_FILE, JSON.stringify(events, null, 2));
40
- }
41
-
42
- // Minimal WebSocket handshake (no dependency needed)
43
- const crypto = require('crypto');
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 /eventsfetch recent events
84
+ // GET /streamSSE 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`);