fluxy-bot 0.4.16 → 0.4.17
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/README.md +371 -0
- package/package.json +1 -1
- package/supervisor/backend.ts +8 -1
- package/supervisor/index.ts +18 -1
package/README.md
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# Fluxy
|
|
2
|
+
|
|
3
|
+
A self-hosted AI agent that runs on the user's machine with a full-stack workspace it can modify, a chat interface for remote control, and a relay system for public access via custom domains.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## System Overview
|
|
8
|
+
|
|
9
|
+
Fluxy is three separate codebases working together:
|
|
10
|
+
|
|
11
|
+
1. **Fluxy Bot** (this repo) -- runs on the user's machine. Supervisor process, worker API, workspace app, chat UI.
|
|
12
|
+
2. **Fluxy Relay** (separate server at `api.fluxy.bot`) -- cloud service that maps `username.fluxy.bot` to the user's Cloudflare tunnel. Routes HTTP and WebSocket traffic.
|
|
13
|
+
3. **Cloudflare Quick Tunnel** -- ephemeral tunnel created by `cloudflared` binary, exposes `localhost:3000` to the internet via a random `*.trycloudflare.com` URL.
|
|
14
|
+
|
|
15
|
+
The relay gives users a permanent domain. The tunnel gives the relay a target. The supervisor ties everything together locally.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Process Architecture
|
|
20
|
+
|
|
21
|
+
When `fluxy start` runs, the CLI spawns a single supervisor process. The supervisor then spawns three child processes and manages their lifecycle:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
CLI (bin/cli.js)
|
|
25
|
+
|
|
|
26
|
+
spawns
|
|
27
|
+
v
|
|
28
|
+
Supervisor (supervisor/index.ts) port 3000 HTTP server + WebSocket + reverse proxy
|
|
29
|
+
|
|
|
30
|
+
+-- Worker (worker/index.ts) port 3001 Express API, SQLite, auth, conversations
|
|
31
|
+
+-- Vite Dev Server port 3002 Serves workspace/client with HMR
|
|
32
|
+
+-- Backend (workspace/backend/) port 3004 User's custom Express server
|
|
33
|
+
+-- cloudflared (tunnel) -- Exposes port 3000 to the internet
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Port allocation: base port (default 3000), worker = base+1, Vite = base+2, backend = base+4.
|
|
37
|
+
|
|
38
|
+
All child processes auto-restart up to 3 times on crash. The supervisor catches SIGINT/SIGTERM and tears everything down gracefully.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Supervisor (supervisor/index.ts)
|
|
43
|
+
|
|
44
|
+
The supervisor is a raw `http.createServer` (no Express) that routes every incoming request:
|
|
45
|
+
|
|
46
|
+
| Path | Target | Notes |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `/app/api/*` | Backend (port 3004) | Strips `/app/api` prefix before forwarding |
|
|
49
|
+
| `/api/*` | Worker (port 3001) | Auth middleware checks Bearer token on mutations |
|
|
50
|
+
| `/fluxy/*` | Static files from `dist-fluxy/` | Pre-built chat SPA, cached aggressively |
|
|
51
|
+
| `/fluxy/widget.js` | Direct file serve | Chat bubble script injected into dashboard |
|
|
52
|
+
| Everything else | Vite dev server (port 3002) | Dashboard + HMR |
|
|
53
|
+
|
|
54
|
+
WebSocket upgrades:
|
|
55
|
+
- `/fluxy/ws` -- Fluxy chat. Auth-gated via query param token. Handled by an in-process `WebSocketServer`.
|
|
56
|
+
- Everything else -- proxied to Vite dev server for HMR.
|
|
57
|
+
|
|
58
|
+
The supervisor also:
|
|
59
|
+
- Manages the Cloudflare tunnel lifecycle (start, stop, health watchdog every 30s)
|
|
60
|
+
- Registers with the relay and maintains heartbeats
|
|
61
|
+
- Runs the Claude Agent SDK when users send chat messages
|
|
62
|
+
- Restarts the backend process when Claude edits workspace files
|
|
63
|
+
- Broadcasts `app:hmr-update` to all connected dashboard clients after file changes
|
|
64
|
+
|
|
65
|
+
### Auth Middleware
|
|
66
|
+
|
|
67
|
+
The supervisor validates Bearer tokens on `/api/*` POST/PUT/DELETE requests by calling the worker's `/api/portal/validate-token` endpoint. Token results are cached for 60 seconds. Auth-exempt routes (login, onboard, health) skip this check.
|
|
68
|
+
|
|
69
|
+
The `/app/api/*` route has no auth -- the user's workspace backend handles its own authentication.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Worker (worker/index.ts)
|
|
74
|
+
|
|
75
|
+
Express server on port 3001. Owns the database and all platform API logic. The supervisor never touches SQLite directly -- everything goes through HTTP.
|
|
76
|
+
|
|
77
|
+
**Database:** `~/.fluxy/memory.db` (SQLite via better-sqlite3, WAL mode)
|
|
78
|
+
|
|
79
|
+
Tables:
|
|
80
|
+
- `conversations` -- chat sessions (id, title, model, session_id, timestamps)
|
|
81
|
+
- `messages` -- individual messages (role, content, tokens, audio, attachments as JSON)
|
|
82
|
+
- `settings` -- key-value store (onboard config, provider, model, portal credentials, etc.)
|
|
83
|
+
- `sessions` -- auth tokens with 7-day expiry
|
|
84
|
+
|
|
85
|
+
Key endpoints:
|
|
86
|
+
- `/api/conversations` -- CRUD for conversations and messages
|
|
87
|
+
- `/api/settings` -- key-value read/write
|
|
88
|
+
- `/api/onboard` -- saves wizard configuration (provider, model, portal password, whisper key)
|
|
89
|
+
- `/api/onboard/status` -- returns current setup state
|
|
90
|
+
- `/api/portal/login` -- password auth (POST with JSON body or GET with Basic Auth header)
|
|
91
|
+
- `/api/portal/validate-token` -- session token validation
|
|
92
|
+
- `/api/whisper/transcribe` -- audio-to-text via OpenAI Whisper API
|
|
93
|
+
- `/api/handle/*` -- register, change, release relay handles
|
|
94
|
+
- `/api/context/current` and `/api/context/set` -- tracks which conversation is active
|
|
95
|
+
|
|
96
|
+
Portal passwords are hashed with scrypt (random 16-byte salt, stored as `salt:hash`).
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Workspace
|
|
101
|
+
|
|
102
|
+
The workspace is a full-stack app template that Claude can freely modify. It lives at `workspace/` in the project and gets copied to `~/.fluxy/workspace/` on first install.
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
workspace/
|
|
106
|
+
client/ React + Vite + Tailwind dashboard
|
|
107
|
+
src/App.tsx Main app entry (error boundary, rebuild overlay, onboard iframe)
|
|
108
|
+
index.html PWA manifest, service worker registration, widget script
|
|
109
|
+
backend/
|
|
110
|
+
index.ts Express server template (reads .env, opens app.db)
|
|
111
|
+
.env Environment variables for the backend
|
|
112
|
+
app.db SQLite database for workspace data
|
|
113
|
+
files/ Attachment storage (audio, images, documents)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The backend runs on port 3004, accessed at `/app/api/*` through the supervisor proxy. The `/app/api` prefix is stripped before reaching the backend, so routes are defined as `/health` not `/app/api/health`.
|
|
117
|
+
|
|
118
|
+
The frontend is served by Vite with HMR. When Claude edits files, Vite picks up changes instantly for the frontend. The supervisor restarts the backend process after any file write by the agent.
|
|
119
|
+
|
|
120
|
+
The workspace is the only directory Claude is allowed to modify. The system prompt explicitly tells it never to touch `supervisor/`, `worker/`, `shared/`, or `bin/`.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Fluxy Chat
|
|
125
|
+
|
|
126
|
+
The chat UI is a standalone React SPA built separately (`vite.fluxy.config.ts` -> `dist-fluxy/`). It runs inside an iframe injected by `widget.js` on the dashboard.
|
|
127
|
+
|
|
128
|
+
### Why an iframe?
|
|
129
|
+
|
|
130
|
+
If Claude introduces a bug that crashes the dashboard, the chat stays alive. The user can still talk to Claude and ask for a fix. The chat and dashboard are completely isolated -- different React trees, different build outputs, different error boundaries.
|
|
131
|
+
|
|
132
|
+
### Widget (supervisor/widget.js)
|
|
133
|
+
|
|
134
|
+
Vanilla JS that injects:
|
|
135
|
+
- A floating video bubble (60px, bottom-right corner)
|
|
136
|
+
- A slide-out panel (480px wide, full screen on mobile) containing the iframe at `/fluxy/`
|
|
137
|
+
- A backdrop for dismissal
|
|
138
|
+
|
|
139
|
+
Communicates with the iframe via `postMessage`:
|
|
140
|
+
- `fluxy:close` -- iframe requests panel close
|
|
141
|
+
- `fluxy:install-app` -- iframe requests PWA install prompt
|
|
142
|
+
- `fluxy:onboard-complete` -- iframe notifies onboarding finished
|
|
143
|
+
- `fluxy:hmr-update` -- supervisor notifies dashboard of file changes (forwarded to parent)
|
|
144
|
+
|
|
145
|
+
### Chat Protocol (WebSocket)
|
|
146
|
+
|
|
147
|
+
Client -> Server:
|
|
148
|
+
- `user:message` -- `{ content, conversationId?, attachments? }` where attachments are `{ type, name, mediaType, data(base64) }`
|
|
149
|
+
- `user:stop` -- abort current agent query
|
|
150
|
+
- `user:clear-context` -- clear conversation and agent session
|
|
151
|
+
- `whisper:transcribe` -- `{ audio: base64 }` (bypasses relay POST limitation)
|
|
152
|
+
- `settings:save` -- `{ ...settings }` (bypasses relay POST limitation)
|
|
153
|
+
|
|
154
|
+
Server -> Client:
|
|
155
|
+
- `bot:typing` -- agent started
|
|
156
|
+
- `bot:token` -- streamed text chunk
|
|
157
|
+
- `bot:tool` -- tool invocation (name, status)
|
|
158
|
+
- `bot:response` -- final complete response
|
|
159
|
+
- `bot:error` -- error message
|
|
160
|
+
- `chat:conversation-created` -- new conversation ID assigned
|
|
161
|
+
- `chat:sync` -- message from another connected client
|
|
162
|
+
- `chat:cleared` -- context was cleared
|
|
163
|
+
- `app:hmr-update` -- file changes detected, dashboard should reload
|
|
164
|
+
|
|
165
|
+
### WebSocket Client (ws-client.ts)
|
|
166
|
+
|
|
167
|
+
Auto-reconnects with exponential backoff (1s -> 8s cap). Queues messages during disconnection. Sends heartbeat pings every 25 seconds. Auth token passed as query parameter on connect.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Claude Agent SDK Integration (supervisor/fluxy-agent.ts)
|
|
172
|
+
|
|
173
|
+
When the configured provider is Anthropic, chat messages are routed through the Claude Agent SDK instead of a raw API call.
|
|
174
|
+
|
|
175
|
+
- Runs with `permissionMode: bypassPermissions` -- full tool access, no confirmation prompts
|
|
176
|
+
- Working directory: `workspace/`
|
|
177
|
+
- Max 50 turns per query
|
|
178
|
+
- System prompt: Claude Code preset + custom addendum from `worker/prompts/fluxy-system-prompt.txt`
|
|
179
|
+
- Sessions tracked in-memory (conversationId -> sessionId) for multi-turn context
|
|
180
|
+
|
|
181
|
+
The agent has access to all Claude Code tools (Read, Write, Edit, Bash, Grep, Glob, etc.). After a query completes, the supervisor checks if Write or Edit tools were used. If so, it restarts the backend and broadcasts an HMR update.
|
|
182
|
+
|
|
183
|
+
OAuth tokens are managed by `worker/claude-auth.ts` using PKCE flow against `claude.ai`. Tokens are stored in the macOS Keychain (primary) or `~/.claude/.credentials.json` (fallback). Refresh tokens are used to renew access tokens with a 5-minute expiry buffer.
|
|
184
|
+
|
|
185
|
+
For non-Anthropic providers (OpenAI, Ollama), the supervisor falls back to `ai.chat()` with simple message history -- no agent tools, no file access.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Tunnel + Relay System
|
|
190
|
+
|
|
191
|
+
### Problem
|
|
192
|
+
|
|
193
|
+
The user's machine is behind NAT. We need a public URL so they can access their bot from their phone.
|
|
194
|
+
|
|
195
|
+
### Solution: Three Tiers
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
Phone browser
|
|
199
|
+
|
|
|
200
|
+
| https://bruno.fluxy.bot
|
|
201
|
+
v
|
|
202
|
+
Fluxy Relay (api.fluxy.bot) Cloud server, maps username -> tunnel URL
|
|
203
|
+
|
|
|
204
|
+
| https://random-abc.trycloudflare.com
|
|
205
|
+
v
|
|
206
|
+
Cloudflare Quick Tunnel Ephemeral tunnel, changes on restart
|
|
207
|
+
|
|
|
208
|
+
| http://localhost:3000
|
|
209
|
+
v
|
|
210
|
+
Supervisor User's machine
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Cloudflare Tunnel (supervisor/tunnel.ts)
|
|
214
|
+
|
|
215
|
+
- Auto-downloads `cloudflared` binary to `~/.fluxy/bin/` on first run
|
|
216
|
+
- Spawns: `cloudflared tunnel --url http://localhost:3000 --no-autoupdate`
|
|
217
|
+
- Extracts tunnel URL from stdout (regex match for `*.trycloudflare.com`)
|
|
218
|
+
- Health check: HEAD request to tunnel URL with 5s timeout
|
|
219
|
+
- Watchdog runs every 30s, detects sleep/wake gaps (>60s between ticks), auto-restarts dead tunnels
|
|
220
|
+
|
|
221
|
+
### Relay Server (separate codebase)
|
|
222
|
+
|
|
223
|
+
Node.js/Express + http-proxy + MongoDB. Hosted on Railway.
|
|
224
|
+
|
|
225
|
+
**Registration flow:**
|
|
226
|
+
1. User picks a username during onboarding
|
|
227
|
+
2. Bot calls `POST /api/register` with username + tier
|
|
228
|
+
3. Relay stores tokenHash (SHA-256) in MongoDB, returns raw token + relay URL
|
|
229
|
+
4. Bot stores token in `~/.fluxy/config.json`
|
|
230
|
+
5. Bot calls `PUT /api/tunnel` with its cloudflared URL
|
|
231
|
+
6. Relay marks bot online, starts accepting proxied traffic
|
|
232
|
+
|
|
233
|
+
**Request proxying:**
|
|
234
|
+
1. Request hits `bruno.fluxy.bot`
|
|
235
|
+
2. Subdomain middleware extracts username + tier from hostname
|
|
236
|
+
3. MongoDB lookup returns the bot's tunnel URL
|
|
237
|
+
4. `proxy.web(req, res, { target: tunnelUrl })` forwards everything -- headers, body, method
|
|
238
|
+
5. Response streams back to the user's browser
|
|
239
|
+
|
|
240
|
+
**Presence:**
|
|
241
|
+
- Bot sends `POST /api/heartbeat` every 30 seconds with its tunnel URL
|
|
242
|
+
- Relay considers a bot stale if no heartbeat in 120 seconds
|
|
243
|
+
- Stale bots get a 503 offline page (auto-refreshes every 15s)
|
|
244
|
+
- On graceful shutdown, bot calls `POST /api/disconnect`
|
|
245
|
+
|
|
246
|
+
**Domain tiers:**
|
|
247
|
+
| Tier | Subdomain | Path shortcut | Cost |
|
|
248
|
+
|---|---|---|---|
|
|
249
|
+
| Premium | `bruno.fluxy.bot` | `fluxy.bot/bruno` | $5/mo |
|
|
250
|
+
| Free | `bruno.my.fluxy.bot` | `my.fluxy.bot/bruno` | Free |
|
|
251
|
+
|
|
252
|
+
Same username can exist on both tiers independently. Compound unique index on `username + tier`.
|
|
253
|
+
|
|
254
|
+
**WebSocket proxying:**
|
|
255
|
+
The relay listens for HTTP upgrade events outside of Express middleware. This is critical -- Express middleware (body parsing, CORS) must not touch WebSocket upgrades. The upgrade handler parses the subdomain, looks up the bot, and calls `proxy.ws()`.
|
|
256
|
+
|
|
257
|
+
### Critical Constraint: POST Bodies Through the Relay
|
|
258
|
+
|
|
259
|
+
The relay's `express.json()` middleware must run AFTER the subdomain resolver, not before. If body parsing runs first, it consumes the request stream and `http-proxy` has nothing to forward. This was a real bug -- the fix was scoping `express.json()` to `/api` routes only, letting proxied traffic pass through with raw streams intact.
|
|
260
|
+
|
|
261
|
+
The Fluxy chat has additional workarounds for this (sending settings and whisper data over WebSocket instead of POST), but with the relay fix these are no longer strictly necessary. They remain as defense-in-depth.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Onboarding (supervisor/chat/OnboardWizard.tsx)
|
|
266
|
+
|
|
267
|
+
Multi-step wizard shown on first launch (inside the Fluxy chat iframe):
|
|
268
|
+
|
|
269
|
+
1. **Welcome** -- intro screen
|
|
270
|
+
2. **Provider** -- choose Claude (Anthropic) or OpenAI Codex
|
|
271
|
+
3. **Model** -- select specific model (Opus, Sonnet, Haiku / GPT-5.x variants)
|
|
272
|
+
4. **Auth** -- OAuth PKCE flow for Claude, or manual API key entry
|
|
273
|
+
5. **Handle** -- register a public username with the relay (checks availability in real time)
|
|
274
|
+
6. **Portal** -- set username/password for remote access
|
|
275
|
+
7. **Whisper** -- optional voice transcription setup with OpenAI key
|
|
276
|
+
8. **Done** -- completion screen
|
|
277
|
+
|
|
278
|
+
Settings are saved via WebSocket (`settings:save` message) to bypass relay POST limitations.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Data Locations
|
|
283
|
+
|
|
284
|
+
| Path | Contents |
|
|
285
|
+
|---|---|
|
|
286
|
+
| `~/.fluxy/config.json` | Port, username, AI provider, relay token, tunnel URL |
|
|
287
|
+
| `~/.fluxy/memory.db` | SQLite -- conversations, messages, settings, sessions |
|
|
288
|
+
| `~/.fluxy/bin/cloudflared` | Cloudflare tunnel binary |
|
|
289
|
+
| `~/.fluxy/workspace/` | User's workspace copy (client, backend, .env, app.db, files/) |
|
|
290
|
+
| `~/.claude/.credentials.json` | Claude OAuth tokens (Linux/Windows) |
|
|
291
|
+
| macOS Keychain `Claude Code-credentials` | Claude OAuth tokens (macOS, source of truth) |
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## File Map
|
|
296
|
+
|
|
297
|
+
### Sacred (never modified by the agent)
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
bin/cli.js CLI entry point, startup sequence, update logic
|
|
301
|
+
supervisor/
|
|
302
|
+
index.ts HTTP server, request routing, WebSocket handler, process orchestration
|
|
303
|
+
worker.ts Worker process spawn/stop/restart
|
|
304
|
+
backend.ts Backend process spawn/stop/restart
|
|
305
|
+
tunnel.ts Cloudflare tunnel lifecycle, health watchdog
|
|
306
|
+
vite-dev.ts Vite dev server startup for dashboard HMR
|
|
307
|
+
fluxy-agent.ts Claude Agent SDK wrapper, session management
|
|
308
|
+
file-saver.ts Attachment storage (audio, images, documents)
|
|
309
|
+
widget.js Chat bubble + panel injected into dashboard
|
|
310
|
+
chat/
|
|
311
|
+
fluxy-main.tsx Chat SPA entry -- auth, WS connection, message routing
|
|
312
|
+
onboard-main.tsx Onboard SPA entry
|
|
313
|
+
OnboardWizard.tsx Multi-step setup wizard
|
|
314
|
+
ARCHITECTURE.md Network topology and relay workaround docs
|
|
315
|
+
src/
|
|
316
|
+
hooks/useFluxyChat.ts Chat state management, message protocol, streaming
|
|
317
|
+
lib/ws-client.ts WebSocket client with reconnect + queue
|
|
318
|
+
lib/auth.ts Token storage and auth fetch wrapper
|
|
319
|
+
components/Chat/ InputBar, MessageBubble, MessageList
|
|
320
|
+
components/LoginScreen.tsx Portal login UI
|
|
321
|
+
worker/
|
|
322
|
+
index.ts Express API server -- all platform endpoints
|
|
323
|
+
db.ts SQLite schema, CRUD operations
|
|
324
|
+
claude-auth.ts Claude OAuth PKCE flow, token refresh, Keychain integration
|
|
325
|
+
codex-auth.ts OpenAI OAuth flow
|
|
326
|
+
prompts/fluxy-system-prompt.txt System prompt that constrains the agent
|
|
327
|
+
shared/
|
|
328
|
+
config.ts Load/save ~/.fluxy/config.json
|
|
329
|
+
paths.ts All path constants (PKG_DIR, DATA_DIR, WORKSPACE_DIR)
|
|
330
|
+
relay.ts Relay API client (register, heartbeat, disconnect, tunnel update)
|
|
331
|
+
ai.ts AI provider abstraction (Anthropic, OpenAI, Ollama)
|
|
332
|
+
logger.ts Colored console logging
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Workspace (agent-modifiable)
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
workspace/
|
|
339
|
+
client/
|
|
340
|
+
index.html Dashboard HTML shell, PWA manifest, widget script tag
|
|
341
|
+
src/main.tsx React DOM entry
|
|
342
|
+
src/App.tsx Dashboard root -- error boundary, rebuild overlay
|
|
343
|
+
src/components/ Dashboard UI components
|
|
344
|
+
backend/
|
|
345
|
+
index.ts Express server template with .env loading and SQLite
|
|
346
|
+
.env Environment variables
|
|
347
|
+
app.db Workspace SQLite database
|
|
348
|
+
files/ Uploaded file storage
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Key Design Decisions
|
|
354
|
+
|
|
355
|
+
**Why a supervisor + worker split instead of one process?**
|
|
356
|
+
Process isolation. If the worker crashes (bad DB migration, OOM), the supervisor keeps running, the tunnel stays up, the chat stays connected. The user can still talk to Claude. Same logic for the backend -- if Claude writes buggy code, only the backend dies.
|
|
357
|
+
|
|
358
|
+
**Why serve the chat from pre-built static files instead of Vite?**
|
|
359
|
+
The chat must survive dashboard crashes. If Vite dies or the workspace frontend throws, the chat iframe loads from `dist-fluxy/` which is just static files. No build process, no dev server dependency.
|
|
360
|
+
|
|
361
|
+
**Why WebSocket for chat instead of HTTP streaming?**
|
|
362
|
+
The relay couldn't reliably forward POST bodies (now fixed). WebSocket was the workaround. It also gives us bidirectional real-time communication, multi-device sync, and heartbeat detection for free.
|
|
363
|
+
|
|
364
|
+
**Why bypassPermissions on the agent?**
|
|
365
|
+
The whole point is that the user talks to Claude from their phone and Claude does whatever's needed. Confirmation prompts would require a terminal session that doesn't exist. The workspace directory boundary + the system prompt are the safety rails.
|
|
366
|
+
|
|
367
|
+
**Why Cloudflare Quick Tunnel instead of a persistent tunnel?**
|
|
368
|
+
Zero configuration. No Cloudflare account needed. The tradeoff is the URL changes on restart, which is why the relay exists -- it provides the stable domain layer on top.
|
|
369
|
+
|
|
370
|
+
**Why two Vite configs?**
|
|
371
|
+
`vite.config.ts` builds the workspace dashboard (user-facing app). `vite.fluxy.config.ts` builds the Fluxy chat SPA. They're separate apps with separate entry points, bundled independently. The chat is pre-built at publish time; the dashboard runs as a dev server with HMR.
|
package/package.json
CHANGED
package/supervisor/backend.ts
CHANGED
|
@@ -5,7 +5,9 @@ import { log } from '../shared/logger.js';
|
|
|
5
5
|
|
|
6
6
|
let child: ChildProcess | null = null;
|
|
7
7
|
let restarts = 0;
|
|
8
|
+
let lastSpawnTime = 0;
|
|
8
9
|
const MAX_RESTARTS = 3;
|
|
10
|
+
const STABLE_THRESHOLD = 30_000; // 30s — if backend ran this long, it wasn't a crash loop
|
|
9
11
|
|
|
10
12
|
export function getBackendPort(basePort: number): number {
|
|
11
13
|
return basePort + 4;
|
|
@@ -13,6 +15,7 @@ export function getBackendPort(basePort: number): number {
|
|
|
13
15
|
|
|
14
16
|
export function spawnBackend(port: number): ChildProcess {
|
|
15
17
|
const backendPath = path.join(PKG_DIR, 'workspace', 'backend', 'index.ts');
|
|
18
|
+
lastSpawnTime = Date.now();
|
|
16
19
|
|
|
17
20
|
child = spawn(process.execPath, ['--import', 'tsx/esm', backendPath], {
|
|
18
21
|
cwd: path.join(PKG_DIR, 'workspace'),
|
|
@@ -31,10 +34,14 @@ export function spawnBackend(port: number): ChildProcess {
|
|
|
31
34
|
child.on('exit', (code) => {
|
|
32
35
|
if (code !== 0 && code !== null) {
|
|
33
36
|
log.warn(`Backend crashed (code ${code})`);
|
|
37
|
+
// If backend was alive for >30s, it's not a crash loop — reset counter
|
|
38
|
+
if (Date.now() - lastSpawnTime > STABLE_THRESHOLD) {
|
|
39
|
+
restarts = 0;
|
|
40
|
+
}
|
|
34
41
|
if (restarts < MAX_RESTARTS) {
|
|
35
42
|
restarts++;
|
|
36
43
|
log.info(`Restarting backend (${restarts}/${MAX_RESTARTS})...`);
|
|
37
|
-
setTimeout(() => spawnBackend(port), 1000);
|
|
44
|
+
setTimeout(() => spawnBackend(port), 1000 * restarts);
|
|
38
45
|
} else {
|
|
39
46
|
log.error('Backend failed too many times. Use Fluxy chat to debug.');
|
|
40
47
|
}
|
package/supervisor/index.ts
CHANGED
|
@@ -426,7 +426,6 @@ export async function startSupervisor() {
|
|
|
426
426
|
resetBackendRestarts();
|
|
427
427
|
stopBackend();
|
|
428
428
|
spawnBackend(backendPort);
|
|
429
|
-
broadcastFluxy('app:hmr-update');
|
|
430
429
|
}
|
|
431
430
|
return; // don't forward bot:done to client
|
|
432
431
|
}
|
|
@@ -565,6 +564,22 @@ export async function startSupervisor() {
|
|
|
565
564
|
spawnWorker(workerPort);
|
|
566
565
|
spawnBackend(backendPort);
|
|
567
566
|
|
|
567
|
+
// Watch workspace/backend/ for file changes — auto-restart backend
|
|
568
|
+
// This catches edits from Claude Code CLI, VS Code, or any external tool
|
|
569
|
+
const backendDir = path.join(PKG_DIR, 'workspace', 'backend');
|
|
570
|
+
let backendRestartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
571
|
+
const backendWatcher = fs.watch(backendDir, { recursive: true }, (_event, filename) => {
|
|
572
|
+
if (!filename || !filename.match(/\.(ts|js|json)$/)) return;
|
|
573
|
+
// Debounce: wait 500ms for rapid edits to settle
|
|
574
|
+
if (backendRestartTimer) clearTimeout(backendRestartTimer);
|
|
575
|
+
backendRestartTimer = setTimeout(() => {
|
|
576
|
+
log.info(`[watcher] Backend file changed: ${filename} — restarting...`);
|
|
577
|
+
resetBackendRestarts();
|
|
578
|
+
stopBackend();
|
|
579
|
+
spawnBackend(backendPort);
|
|
580
|
+
}, 500);
|
|
581
|
+
});
|
|
582
|
+
|
|
568
583
|
// Tunnel
|
|
569
584
|
let tunnelUrl: string | null = null;
|
|
570
585
|
if (config.tunnel.enabled) {
|
|
@@ -633,6 +648,8 @@ export async function startSupervisor() {
|
|
|
633
648
|
// Shutdown
|
|
634
649
|
const shutdown = async () => {
|
|
635
650
|
log.info('Shutting down...');
|
|
651
|
+
backendWatcher.close();
|
|
652
|
+
if (backendRestartTimer) clearTimeout(backendRestartTimer);
|
|
636
653
|
if (watchdogInterval) clearInterval(watchdogInterval);
|
|
637
654
|
stopHeartbeat();
|
|
638
655
|
const latestConfig = loadConfig();
|