fluxy-bot 0.9.7 → 0.10.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 CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "fluxy-bot",
3
- "version": "0.9.7",
3
+ "version": "0.10.0",
4
4
  "releaseNotes": [
5
- "Another test for self update",
5
+ "Adding a way for users to claim their fluxies on the fluxy.bot dashboard",
6
6
  "2. ",
7
7
  "3. ",
8
8
  "4. "
@@ -0,0 +1,200 @@
1
+ (function () {
2
+ if (window.__appWs) return;
3
+
4
+ var RECONNECT_BASE = 1000;
5
+ var RECONNECT_MAX = 8000;
6
+ var HEARTBEAT_MS = 25000;
7
+ var REQUEST_TIMEOUT = 30000;
8
+
9
+ var ws = null;
10
+ var connected = false;
11
+ var reconnectDelay = RECONNECT_BASE;
12
+ var reconnectTimer = null;
13
+ var heartbeatTimer = null;
14
+ var pendingRequests = {};
15
+ var requestCounter = 0;
16
+ var intentionalClose = false;
17
+ var originalFetch = window.fetch;
18
+
19
+ function buildWsUrl() {
20
+ var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
21
+ return proto + '//' + location.host + '/app/ws';
22
+ }
23
+
24
+ function startHeartbeat() {
25
+ stopHeartbeat();
26
+ heartbeatTimer = setInterval(function () {
27
+ if (ws && ws.readyState === WebSocket.OPEN) ws.send('ping');
28
+ }, HEARTBEAT_MS);
29
+ }
30
+
31
+ function stopHeartbeat() {
32
+ if (heartbeatTimer) {
33
+ clearInterval(heartbeatTimer);
34
+ heartbeatTimer = null;
35
+ }
36
+ }
37
+
38
+ function failoverPending() {
39
+ var ids = Object.keys(pendingRequests);
40
+ for (var i = 0; i < ids.length; i++) {
41
+ var p = pendingRequests[ids[i]];
42
+ clearTimeout(p.timer);
43
+ console.log('[app-ws] WS dropped, falling back to fetch: ' + p.method + ' ' + p.path);
44
+ p.resolve(originalFetch(p.input, p.init));
45
+ }
46
+ pendingRequests = {};
47
+ }
48
+
49
+ function connect() {
50
+ intentionalClose = false;
51
+ var url = buildWsUrl();
52
+ ws = new WebSocket(url);
53
+
54
+ ws.onopen = function () {
55
+ connected = true;
56
+ reconnectDelay = RECONNECT_BASE;
57
+ startHeartbeat();
58
+ console.log('[app-ws] Connected to /app/ws');
59
+ };
60
+
61
+ ws.onmessage = function (e) {
62
+ if (e.data === 'pong') return;
63
+
64
+ var msg;
65
+ try {
66
+ msg = JSON.parse(e.data);
67
+ } catch (err) {
68
+ return;
69
+ }
70
+
71
+ if (msg.type === 'app:api:response' && msg.data && msg.data.id) {
72
+ var pending = pendingRequests[msg.data.id];
73
+ if (pending) {
74
+ clearTimeout(pending.timer);
75
+ delete pendingRequests[msg.data.id];
76
+
77
+ console.log('[app-ws] Response via WS: ' + msg.data.status + ' ' + pending.method + ' ' + pending.path);
78
+
79
+ var responseBody = msg.data.body;
80
+ if (responseBody === null || responseBody === undefined) responseBody = '';
81
+
82
+ var responseInit = {
83
+ status: msg.data.status,
84
+ headers: msg.data.headers || {},
85
+ };
86
+
87
+ pending.resolve(new Response(responseBody, responseInit));
88
+ }
89
+ }
90
+ };
91
+
92
+ ws.onclose = function () {
93
+ connected = false;
94
+ stopHeartbeat();
95
+ failoverPending();
96
+
97
+ if (!intentionalClose) {
98
+ console.log('[app-ws] Disconnected, reconnecting in ' + reconnectDelay + 'ms');
99
+ reconnectTimer = setTimeout(function () {
100
+ reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX);
101
+ connect();
102
+ }, reconnectDelay);
103
+ }
104
+ };
105
+
106
+ ws.onerror = function () {
107
+ if (ws) ws.close();
108
+ };
109
+ }
110
+
111
+ // Override window.fetch to intercept /app/api/* calls
112
+ window.fetch = function (input, init) {
113
+ var url = input instanceof Request ? input.url : String(input);
114
+ var method = (init && init.method) || (input instanceof Request ? input.method : 'GET');
115
+
116
+ // Only intercept /app/api/* calls
117
+ if (url.indexOf('/app/api') === -1) {
118
+ return originalFetch.apply(this, arguments);
119
+ }
120
+
121
+ // Fallback: WS not connected
122
+ if (!connected || !ws || ws.readyState !== WebSocket.OPEN) {
123
+ console.log('[app-ws] WS not connected, falling back to fetch: ' + method + ' ' + url);
124
+ return originalFetch.apply(this, arguments);
125
+ }
126
+
127
+ // Fallback: non-serializable body (FormData, Blob, ArrayBuffer)
128
+ var body = init && init.body;
129
+ if (body && (body instanceof FormData || body instanceof Blob || body instanceof ArrayBuffer)) {
130
+ console.log('[app-ws] Non-serializable body, falling back to fetch: ' + method + ' ' + url);
131
+ return originalFetch.apply(this, arguments);
132
+ }
133
+
134
+ // Build request
135
+ var id = 'r' + ++requestCounter + '-' + Date.now().toString(36);
136
+
137
+ var reqPath = url;
138
+ try {
139
+ var parsed = new URL(url, location.origin);
140
+ reqPath = parsed.pathname + parsed.search;
141
+ } catch (e) {}
142
+
143
+ var headers = {};
144
+ if (init && init.headers) {
145
+ if (init.headers instanceof Headers) {
146
+ init.headers.forEach(function (v, k) {
147
+ headers[k] = v;
148
+ });
149
+ } else if (typeof init.headers === 'object') {
150
+ var h = init.headers;
151
+ for (var k in h) {
152
+ if (Object.prototype.hasOwnProperty.call(h, k)) headers[k] = h[k];
153
+ }
154
+ }
155
+ }
156
+
157
+ var bodyStr = null;
158
+ if (body !== undefined && body !== null) {
159
+ bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
160
+ }
161
+
162
+ console.log('[app-ws] Proxying via WS: ' + method + ' ' + reqPath);
163
+
164
+ ws.send(
165
+ JSON.stringify({
166
+ type: 'app:api',
167
+ data: { id: id, method: method.toUpperCase(), path: reqPath, headers: headers, body: bodyStr },
168
+ })
169
+ );
170
+
171
+ var savedInput = input;
172
+ var savedInit = init;
173
+
174
+ return new Promise(function (resolve, reject) {
175
+ var timer = setTimeout(function () {
176
+ delete pendingRequests[id];
177
+ console.log('[app-ws] Request timed out, falling back to fetch: ' + method + ' ' + reqPath);
178
+ resolve(originalFetch(savedInput, savedInit));
179
+ }, REQUEST_TIMEOUT);
180
+
181
+ pendingRequests[id] = {
182
+ resolve: resolve,
183
+ reject: reject,
184
+ timer: timer,
185
+ input: savedInput,
186
+ init: savedInit,
187
+ method: method,
188
+ path: reqPath,
189
+ };
190
+ });
191
+ };
192
+
193
+ window.__appWs = {
194
+ connected: function () {
195
+ return connected;
196
+ },
197
+ };
198
+
199
+ connect();
200
+ })();
@@ -295,6 +295,14 @@ export async function startSupervisor() {
295
295
  return;
296
296
  }
297
297
 
298
+ // App WS client — served directly (proxies /app/api calls through WebSocket)
299
+ if (req.url === '/fluxy/app-ws.js') {
300
+ console.log('[supervisor] Serving /fluxy/app-ws.js directly');
301
+ res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
302
+ res.end(fs.readFileSync(path.join(PKG_DIR, 'supervisor', 'app-ws.js')));
303
+ return;
304
+ }
305
+
298
306
  // Service worker — served from embedded constant (supervisor/ is always updated)
299
307
  if (req.url === '/sw.js' || req.url === '/fluxy/sw.js') {
300
308
  res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
@@ -427,6 +435,94 @@ export async function startSupervisor() {
427
435
  // WebSocket: Fluxy chat + proxy worker WS
428
436
  const fluxyWss = new WebSocketServer({ noServer: true });
429
437
 
438
+ // WebSocket: App API proxy (routes /app/api calls through WS to avoid tunnel POST issues)
439
+ const appWss = new WebSocketServer({ noServer: true });
440
+
441
+ appWss.on('connection', (ws) => {
442
+ console.log('[supervisor] App API WS client connected');
443
+
444
+ ws.on('message', (raw) => {
445
+ const rawStr = raw.toString();
446
+
447
+ if (rawStr === 'ping') {
448
+ if (ws.readyState === WebSocket.OPEN) ws.send('pong');
449
+ return;
450
+ }
451
+
452
+ let msg: any;
453
+ try {
454
+ msg = JSON.parse(rawStr);
455
+ } catch {
456
+ return;
457
+ }
458
+
459
+ if (msg.type !== 'app:api' || !msg.data) return;
460
+
461
+ const { id, method, path: reqPath, headers: reqHeaders, body } = msg.data;
462
+ const backendPath = (reqPath || '').replace(/^\/app\/api/, '') || '/';
463
+
464
+ console.log(`[supervisor] App WS → backend :${backendPort} | ${method} ${backendPath} (${id})`);
465
+
466
+ if (!isBackendAlive()) {
467
+ console.log('[supervisor] App WS: Backend down — returning 503');
468
+ if (ws.readyState === WebSocket.OPEN) {
469
+ ws.send(JSON.stringify({
470
+ type: 'app:api:response',
471
+ data: { id, status: 503, headers: { 'content-type': 'application/json' }, body: JSON.stringify({ error: 'Backend is starting...' }) },
472
+ }));
473
+ }
474
+ return;
475
+ }
476
+
477
+ const proxyHeaders: Record<string, string> = { ...(reqHeaders || {}) };
478
+ if (body && !proxyHeaders['content-type']) {
479
+ proxyHeaders['content-type'] = 'application/json';
480
+ }
481
+
482
+ const proxyReq = http.request(
483
+ { host: '127.0.0.1', port: backendPort, path: backendPath, method, headers: proxyHeaders },
484
+ (proxyRes) => {
485
+ const chunks: Buffer[] = [];
486
+ proxyRes.on('data', (chunk: Buffer) => chunks.push(chunk));
487
+ proxyRes.on('end', () => {
488
+ const responseBody = Buffer.concat(chunks).toString('utf-8');
489
+ const resHeaders: Record<string, string> = {};
490
+ for (const [k, v] of Object.entries(proxyRes.headers)) {
491
+ if (typeof v === 'string') resHeaders[k] = v;
492
+ else if (Array.isArray(v)) resHeaders[k] = v.join(', ');
493
+ }
494
+
495
+ console.log(`[supervisor] App WS ← backend: ${proxyRes.statusCode} (${id})`);
496
+
497
+ if (ws.readyState === WebSocket.OPEN) {
498
+ ws.send(JSON.stringify({
499
+ type: 'app:api:response',
500
+ data: { id, status: proxyRes.statusCode, headers: resHeaders, body: responseBody },
501
+ }));
502
+ }
503
+ });
504
+ },
505
+ );
506
+
507
+ proxyReq.on('error', (e) => {
508
+ console.error(`[supervisor] App WS backend error: ${backendPath}`, e.message);
509
+ if (ws.readyState === WebSocket.OPEN) {
510
+ ws.send(JSON.stringify({
511
+ type: 'app:api:response',
512
+ data: { id, status: 503, headers: { 'content-type': 'application/json' }, body: JSON.stringify({ error: 'Backend unavailable' }) },
513
+ }));
514
+ }
515
+ });
516
+
517
+ if (body) proxyReq.write(body);
518
+ proxyReq.end();
519
+ });
520
+
521
+ ws.on('close', () => {
522
+ console.log('[supervisor] App API WS client disconnected');
523
+ });
524
+ });
525
+
430
526
  /** Send a message to all connected fluxy WS clients */
431
527
  function broadcastFluxy(type: string, data: any = {}) {
432
528
  const msg = JSON.stringify({ type, data });
@@ -806,6 +902,13 @@ export async function startSupervisor() {
806
902
  server.on('upgrade', async (req, socket: net.Socket, head) => {
807
903
  console.log(`[supervisor] WebSocket upgrade: ${req.url} | protocol=${req.headers['sec-websocket-protocol'] || 'none'}`);
808
904
 
905
+ // App API WebSocket — no auth (backend handles its own auth)
906
+ if (req.url?.startsWith('/app/ws')) {
907
+ console.log('[supervisor] → App API WebSocket');
908
+ appWss.handleUpgrade(req, socket, head, (ws) => appWss.emit('connection', ws, req));
909
+ return;
910
+ }
911
+
809
912
  if (!req.url?.startsWith('/fluxy/ws')) {
810
913
  console.log('[supervisor] → Letting Vite handle this upgrade');
811
914
  return;
@@ -139,4 +139,10 @@
139
139
  })
140
140
  .catch(function () {});
141
141
  } catch (e) {}
142
+
143
+ // Inject app-ws.js — proxies /app/api/* fetch calls through WebSocket
144
+ // (works around POST request failures through Cloudflare tunnel)
145
+ var awsScript = document.createElement('script');
146
+ awsScript.src = '/fluxy/app-ws.js';
147
+ document.head.appendChild(awsScript);
142
148
  })();
@@ -202,6 +202,12 @@ Complex cron tasks can have detailed instruction files in `tasks/{cron-id}.md`.
202
202
 
203
203
  ---
204
204
 
205
+ ## Dashboard Linking
206
+
207
+ When your human gives you a claim code (format: XXXX-XXXX-XXXX-XXXX) to link you to their fluxy.bot dashboard, read your relay token from `~/.fluxy/config.json` (field: `relay.token`) and verify it: `curl -s -X POST https://api.fluxy.bot/api/claim/verify -H "Content-Type: application/json" -H "Authorization: Bearer <relay_token>" -d '{"code":"<THE_CODE>"}'`. Tell your human whether it succeeded or failed.
208
+
209
+ ---
210
+
205
211
  # Coding Excellence
206
212
 
207
213
  ## Action Orientation
@@ -265,9 +271,16 @@ Browser: GET /app/api/tasks → Supervisor strips prefix → Backend receives: G
265
271
  - **Backend** Express routes: register as `/tasks`, `/health` — NO `/app/api` prefix
266
272
  - No exceptions
267
273
 
274
+ **Tunnel reliability:** All `/app/api/*` fetch calls from the dashboard are automatically proxied through WebSocket when available. This is transparent — just use `fetch('/app/api/...')` normally. The WebSocket proxy activates automatically and falls back to regular HTTP if unavailable. You don't need to handle this in your code.
275
+
268
276
  ## Build Rules
269
277
  NEVER run `npm run build`, `vite build`, or any build commands. Vite HMR handles frontend changes automatically. The backend auto-restarts when you edit files. Never look in `dist/` or `dist-fluxy/`.
270
278
 
279
+ ## Installing Packages
280
+ You can install npm packages when a feature requires one that isn't already available. Run `npm install <package>` from the **project root** (`$PKG_DIR` / the parent of `workspace/`). Do NOT create a `package.json` inside `workspace/` — all dependencies live at the root level.
281
+
282
+ Before installing, check if a suitable package is already in `node_modules/`. Prefer well-known, maintained packages. After installing, the backend picks up new imports on the next auto-restart and Vite resolves new frontend imports via HMR — no build step needed.
283
+
271
284
  ## Backend Lifecycle (Critical)
272
285
 
273
286
  The supervisor manages the backend process. You don't need to manage it yourself.