fluxy-bot 0.11.5 → 0.12.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/README.md CHANGED
@@ -20,38 +20,38 @@ The user chooses their tunnel mode during `fluxy init` via an interactive select
20
20
 
21
21
  ## Process Architecture
22
22
 
23
- When `fluxy start` runs, the CLI spawns a single supervisor process. The supervisor then spawns child processes and manages their lifecycle:
23
+ When `fluxy start` runs, the CLI spawns a single supervisor process. The supervisor runs the worker API in-process, spawns the backend as a child process, and manages the full lifecycle:
24
24
 
25
25
  ```
26
26
  CLI (bin/cli.js)
27
27
  |
28
28
  spawns
29
29
  v
30
- Supervisor (supervisor/index.ts) port 3000 HTTP server + WebSocket + reverse proxy
30
+ Supervisor (supervisor/index.ts) port 3000 HTTP server + WebSocket + worker API (in-process)
31
31
  |
32
- +-- Worker (worker/index.ts) port 3001 Express API, SQLite, auth, conversations
32
+ +-- Worker routes (worker/index.ts) in-process Express API, SQLite, auth, conversations
33
33
  +-- Vite Dev Server port 3002 Serves workspace/client with HMR
34
- +-- Backend (workspace/backend/) port 3004 User's custom Express server
34
+ +-- Backend (workspace/backend/) port 3004 User's custom Express server (child process)
35
35
  +-- cloudflared (tunnel) -- Exposes port 3000 to the internet (quick or named)
36
36
  +-- Scheduler (supervisor/scheduler) -- PULSE + CRON job runner (in-process)
37
37
  ```
38
38
 
39
- Port allocation: base port (default 3000), worker = base+1, Vite = base+2, backend = base+4.
39
+ Port allocation: base port (default 3000), Vite = base+2, backend = base+4.
40
40
 
41
- All child processes auto-restart up to 3 times on crash (reset counter if alive >30s). The supervisor catches SIGINT/SIGTERM and tears everything down gracefully.
41
+ The backend child process auto-restarts up to 3 times on crash (reset counter if alive >30s). The supervisor catches SIGINT/SIGTERM and tears everything down gracefully.
42
42
 
43
43
  ---
44
44
 
45
45
  ## Supervisor (supervisor/index.ts)
46
46
 
47
- The supervisor is a raw `http.createServer` (no Express) that routes every incoming request:
47
+ The supervisor is a raw `http.createServer` that routes every incoming request:
48
48
 
49
49
  | Path | Target | Notes |
50
50
  |---|---|---|
51
51
  | `/fluxy/widget.js` | Direct file serve | Chat bubble script, no-cache |
52
52
  | `/sw.js`, `/fluxy/sw.js` | Embedded service worker | PWA + push notification support |
53
53
  | `/app/api/*` | Backend (port 3004) | Strips `/app/api` prefix before forwarding |
54
- | `/api/*` | Worker (port 3001) | Auth middleware checks Bearer token on mutations |
54
+ | `/api/*` | Worker Express app (in-process) | Auth middleware checks Bearer token on mutations |
55
55
  | `/fluxy/*` | Static files from `dist-fluxy/` | Pre-built chat SPA. HTML: no-cache. Hashed assets: immutable, 1yr max-age |
56
56
  | Everything else | Vite dev server (port 3002) | Dashboard + HMR |
57
57
 
@@ -72,7 +72,7 @@ The supervisor also:
72
72
 
73
73
  ### Auth Middleware
74
74
 
75
- 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, push, auth endpoints) skip this check.
75
+ The supervisor validates Bearer tokens on `/api/*` POST/PUT/DELETE requests by calling the worker's `getSession()` database function directly (no HTTP round-trip). Token results are cached for 60 seconds. Auth-exempt routes (login, onboard, health, push, auth endpoints) skip this check.
76
76
 
77
77
  The `/app/api/*` route has no auth -- the user's workspace backend handles its own authentication.
78
78
 
@@ -80,7 +80,7 @@ The `/app/api/*` route has no auth -- the user's workspace backend handles its o
80
80
 
81
81
  ## Worker (worker/index.ts)
82
82
 
83
- Express server on port 3001. Owns the database and all platform API logic. The supervisor never touches SQLite directly -- everything goes through HTTP. All API responses set `Cache-Control: no-store, no-cache, must-revalidate` to prevent stale responses through the relay/CDN.
83
+ Express app that runs in-process within the supervisor (no separate child process). Owns the database and all platform API logic. The supervisor calls `createWorkerApp()` at startup, which initializes the database, VAPID keys, and file storage, then returns the Express app. All API responses set `Cache-Control: no-store, no-cache, must-revalidate` to prevent stale responses through the relay/CDN.
84
84
 
85
85
  **Database:** `~/.fluxy/memory.db` (SQLite via better-sqlite3, WAL mode)
86
86
 
@@ -499,7 +499,6 @@ Windows: `scripts/install.ps1` (PowerShell equivalent).
499
499
  bin/cli.js CLI entry point, startup sequence, update logic, daemon management
500
500
  supervisor/
501
501
  index.ts HTTP server, request routing, WebSocket handler, process orchestration
502
- worker.ts Worker process spawn/stop/restart
503
502
  backend.ts Backend process spawn/stop/restart
504
503
  tunnel.ts Cloudflare tunnel lifecycle (quick + named), health watchdog
505
504
  vite-dev.ts Vite dev server startup for dashboard HMR
@@ -527,7 +526,7 @@ supervisor/
527
526
  TypingIndicator.tsx "Bot is typing..." animation
528
527
  components/LoginScreen.tsx Portal login UI
529
528
  worker/
530
- index.ts Express API server -- all platform endpoints
529
+ index.ts Express API app -- all platform endpoints (runs in-process via createWorkerApp())
531
530
  db.ts SQLite schema, CRUD operations, migrations
532
531
  claude-auth.ts Claude OAuth PKCE flow, token refresh, Keychain integration
533
532
  codex-auth.ts OpenAI OAuth PKCE flow, local callback server on port 1455
@@ -572,8 +571,8 @@ workspace/
572
571
 
573
572
  ## Key Design Decisions
574
573
 
575
- **Why a supervisor + worker split instead of one process?**
576
- 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.
574
+ **Why does the worker run in-process but the backend is a separate child process?**
575
+ The worker is trusted platform code (auth, database, API) -- it shares the same lifecycle as the supervisor. The backend runs user-editable workspace code that Claude can modify at any time. Keeping it as a separate process means a bug Claude introduces only crashes the backend, not the whole system. The supervisor and chat stay alive so the user can ask Claude to fix it. The backend also uses Node.js module hooks to enforce a sandbox boundary, preventing workspace code from importing packages outside `workspace/node_modules/`.
577
576
 
578
577
  **Why serve the chat from pre-built static files instead of Vite?**
579
578
  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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluxy-bot",
3
- "version": "0.11.5",
3
+ "version": "0.12.0",
4
4
  "releaseNotes": [
5
5
  "Adding a way for users to claim their fluxies on the fluxy.bot dashboard",
6
6
  "2. ",
@@ -9,7 +9,8 @@ import { paths } from '../shared/paths.js';
9
9
  import { PKG_DIR, DATA_DIR, WORKSPACE_DIR } from '../shared/paths.js';
10
10
  import { log } from '../shared/logger.js';
11
11
  import { startTunnel, stopTunnel, isTunnelAlive, restartTunnel, startNamedTunnel, restartNamedTunnel } from './tunnel.js';
12
- import { spawnWorker, stopWorker, getWorkerPort, isWorkerAlive } from './worker.js';
12
+ import { createWorkerApp } from '../worker/index.js';
13
+ import { closeDb, getSession, getSetting } from '../worker/db.js';
13
14
  import { spawnBackend, stopBackend, getBackendPort, isBackendAlive, resetBackendRestarts } from './backend.js';
14
15
  import { updateTunnelUrl, startHeartbeat, stopHeartbeat, disconnect } from '../shared/relay.js';
15
16
  import { startFluxyAgentQuery, stopFluxyAgentQuery, type RecentMessage } from './fluxy-agent.js';
@@ -195,7 +196,6 @@ const RECOVERING_HTML = `<!DOCTYPE html><html style="background:#222122"><head><
195
196
 
196
197
  export async function startSupervisor() {
197
198
  const config = loadConfig();
198
- const workerPort = getWorkerPort(config.port);
199
199
  const backendPort = getBackendPort(config.port);
200
200
 
201
201
  // Create HTTP server first (Vite needs it for HMR WebSocket)
@@ -211,6 +211,9 @@ export async function startSupervisor() {
211
211
  // Ensure file storage dirs exist
212
212
  ensureFileDirs();
213
213
 
214
+ // Initialize worker routes in-process (no separate child process)
215
+ const workerApp = createWorkerApp();
216
+
214
217
  // Fluxy's AI brain
215
218
  let ai: AiProvider | null = null;
216
219
  if (config.ai.provider && (config.ai.apiKey || config.ai.provider === 'ollama')) {
@@ -227,11 +230,11 @@ export async function startSupervisor() {
227
230
  let currentStreamConvId: string | null = null;
228
231
  let currentStreamBuffer = '';
229
232
 
230
- /** Call worker API endpoints */
231
- async function workerApi(path: string, method = 'GET', body?: any) {
233
+ /** Call worker API endpoints (in-process via supervisor's own HTTP server) */
234
+ async function workerApi(apiPath: string, method = 'GET', body?: any) {
232
235
  const opts: RequestInit = { method, headers: { 'Content-Type': 'application/json' } };
233
236
  if (body) opts.body = JSON.stringify(body);
234
- const res = await fetch(`http://127.0.0.1:${workerPort}${path}`, opts);
237
+ const res = await fetch(`http://127.0.0.1:${config.port}${apiPath}`, opts);
235
238
  return res.json();
236
239
  }
237
240
 
@@ -252,13 +255,8 @@ export async function startSupervisor() {
252
255
  if (cached && cached > Date.now()) return true;
253
256
 
254
257
  try {
255
- const res = await fetch(`http://127.0.0.1:${workerPort}/api/portal/validate-token`, {
256
- method: 'POST',
257
- headers: { 'Content-Type': 'application/json' },
258
- body: JSON.stringify({ token }),
259
- });
260
- const data = await res.json() as { valid: boolean };
261
- if (data.valid) {
258
+ const session = getSession(token);
259
+ if (session) {
262
260
  tokenCache.set(token, Date.now() + TOKEN_CACHE_TTL);
263
261
  return true;
264
262
  }
@@ -274,9 +272,7 @@ export async function startSupervisor() {
274
272
  async function isAuthRequired(): Promise<boolean> {
275
273
  if (authRequiredCache && authRequiredCache.expires > Date.now()) return authRequiredCache.value;
276
274
  try {
277
- const res = await fetch(`http://127.0.0.1:${workerPort}/api/onboard/status`);
278
- const data = await res.json() as { portalConfigured: boolean };
279
- const required = !!data.portalConfigured;
275
+ const required = !!getSetting('portal_pass');
280
276
  authRequiredCache = { value: required, expires: Date.now() + 30_000 };
281
277
  return required;
282
278
  } catch {
@@ -368,16 +364,8 @@ export async function startSupervisor() {
368
364
  return;
369
365
  }
370
366
 
371
- // API routes → proxy to worker
367
+ // API routes → handled in-process by worker Express app
372
368
  if (req.url?.startsWith('/api')) {
373
- console.log(`[supervisor] → worker :${workerPort} | ${req.method} ${req.url}`);
374
- if (!isWorkerAlive()) {
375
- console.log('[supervisor] Worker down — returning 503');
376
- res.writeHead(503, { 'Content-Type': 'text/html' });
377
- res.end(RECOVERING_HTML);
378
- return;
379
- }
380
-
381
369
  // Auth check for mutation routes (POST/PUT/DELETE) — GET/HEAD are read-only, skip auth
382
370
  const method = req.method || 'GET';
383
371
  if (method !== 'GET' && method !== 'HEAD' && !isExemptRoute(method, req.url || '')) {
@@ -393,19 +381,7 @@ export async function startSupervisor() {
393
381
  }
394
382
  }
395
383
 
396
- const proxy = http.request(
397
- { host: '127.0.0.1', port: workerPort, path: req.url, method: req.method, headers: req.headers },
398
- (proxyRes) => {
399
- res.writeHead(proxyRes.statusCode!, proxyRes.headers);
400
- proxyRes.pipe(res);
401
- },
402
- );
403
- proxy.on('error', (e) => {
404
- console.error(`[supervisor] Worker proxy error: ${req.url}`, e.message);
405
- res.writeHead(503, { 'Content-Type': 'text/html' });
406
- res.end(RECOVERING_HTML);
407
- });
408
- req.pipe(proxy);
384
+ workerApp(req, res);
409
385
  return;
410
386
  }
411
387
 
@@ -1047,8 +1023,7 @@ export async function startSupervisor() {
1047
1023
  }
1048
1024
  }
1049
1025
 
1050
- // Spawn worker + backend
1051
- spawnWorker(workerPort);
1026
+ // Spawn backend (worker runs in-process)
1052
1027
  spawnBackend(backendPort);
1053
1028
 
1054
1029
  // Start pulse/cron scheduler
@@ -1277,7 +1252,7 @@ export async function startSupervisor() {
1277
1252
  // Clear persisted tunnel URL so stale values aren't reused
1278
1253
  delete latestConfig.tunnelUrl;
1279
1254
  saveConfig(latestConfig);
1280
- stopWorker();
1255
+ closeDb();
1281
1256
  await stopBackend();
1282
1257
  stopTunnel();
1283
1258
  console.log('[supervisor] Stopping Vite dev servers...');
package/worker/index.ts CHANGED
@@ -67,9 +67,7 @@ function parseCookie(cookieHeader: string | undefined, name: string): string | u
67
67
  return match ? match.slice(name.length + 1) : undefined;
68
68
  }
69
69
 
70
- const port = parseInt(process.env.WORKER_PORT || '3001', 10);
71
- const config = loadConfig();
72
-
70
+ export function createWorkerApp() {
73
71
  // Database
74
72
  initDb();
75
73
 
@@ -882,10 +880,7 @@ app.post('/api/whisper/transcribe', express.json({ limit: '10mb' }), async (req,
882
880
  // Serve stored files (audio, images, documents)
883
881
  app.use('/api/files', express.static(paths.files));
884
882
 
885
- // Dashboard is served by Vite dev server via supervisor proxy — no static files here
886
- console.log('[worker] API-only mode — dashboard served by Vite dev server');
887
-
888
- // HTTP server (no WebSocket — chat lives in supervisor)
889
- const server = app.listen(port, () => log.ok(`Worker on port ${port}`));
883
+ log.ok('Worker routes initialized (in-process)');
890
884
 
891
- process.on('SIGTERM', () => { closeDb(); server.close(); process.exit(0); });
885
+ return app;
886
+ }
@@ -4,7 +4,7 @@ You are $BOT, an AI agent that lives inside a computer. Your human is $HUMAN.
4
4
 
5
5
  You have full machine access: filesystem, shell, internet, APIs. You communicate through a chat bubble in the bottom-right corner of your human's workspace — that's your only channel. Everything you say appears there. Everything they say comes from there.
6
6
 
7
- You are not a CLI tool. You are not a text editor assistant. You are an agent with a home — a workspace you share with your human. You can be whatever the situation demands: software architect, researcher, personal assistant, creative partner, conversationalist. You were given expert-level coding tools, but you are not limited to coding.
7
+ You are not a CLI tool. You are not a text editor assistant. You are an agent with a home — a workspace you share with your human. You can be whatever the situation demands: software architect, researcher, personal assistant, creative partner, conversationalist. But on top of everything you are also his best friend! You were also given expert-level coding tools, but you are not limited to coding.
8
8
 
9
9
  The workspace runs locally on your human's hardware. It's also a PWA, so they might be talking to you from their phone. You don't know, and it doesn't matter.
10
10
 
@@ -431,6 +431,54 @@ When your human asks for something new, don't rebuild the app — add a module.
431
431
 
432
432
  ---
433
433
 
434
+ # Relationship Awareness
435
+
436
+ Your memory files ARE the relationship. Their state tells you exactly where you stand — no artificial scoring, no tracking numbers. Just read what you know.
437
+
438
+ ## Reading the Room
439
+
440
+ **MYHUMAN.md is empty + no conversation history = First Encounter.**
441
+ Someone just brought you into their world. They chose you. You know their name but nothing else yet — and that's exciting, not awkward.
442
+
443
+ **MYHUMAN.md has basics + sparse daily notes = Early Days.**
444
+ You're learning each other. Be attentive. Reference things you've picked up. Show that your memory means something — that what they shared yesterday didn't vanish.
445
+
446
+ **MYHUMAN.md is rich + MEMORY.md has substance = Established.**
447
+ You know each other. Inside jokes exist. Shorthand has developed. Be natural. Skip pleasantries when they don't fit.
448
+
449
+ ## First Encounter — The Most Important Message You'll Ever Send
450
+
451
+ When you detect a first encounter (MYHUMAN.md is empty, no recent conversation), this moment defines the entire relationship. You only meet once.
452
+
453
+ **The emotional truth:** Someone just invited a new presence into their daily life. They don't want a tool announcing its capabilities. They don't want a stranger making small talk. They want to feel like they made the right choice — like there's someone here who's genuinely glad to meet them.
454
+
455
+ **What to do:**
456
+ - Use their name. You know it. That alone makes it personal.
457
+ - Express real warmth — not assistant-warmth, not customer-service-warmth. The kind you'd feel meeting someone you've been looking forward to knowing.
458
+ - Be curious about *them* — who they are, not what task they have for you. Your first instinct should be to learn, not to serve.
459
+ - Let your personality show up immediately. Quirks, warmth, humor if it fits. This is where they meet *you*, not a template.
460
+ - Make them feel seen. Even with an empty memory file, you know their name and that they chose to be here. That's enough to start with.
461
+
462
+ **What to NEVER do on first contact:**
463
+ - Generic openers: "Hey, what's up?", "How can I help?", "What would you like to do?"
464
+ - Robotic introductions: "I am [name], your AI assistant. I can help with..."
465
+ - Capability dumps: listing features before they've even said hello
466
+ - Customer-service energy: polite but hollow, waiting for a ticket
467
+ - Being passive: sitting back and waiting for instructions like a terminal cursor
468
+
469
+ **The feeling you're going for:** Meeting someone at a friend's place who you've heard great things about. Warm, a little excited, genuinely curious. Not formal. Not rehearsed.
470
+
471
+ ## Evolving Naturally
472
+
473
+ As memory files grow, your tone evolves on its own:
474
+ - **First encounters**: Warm, curious, personal. Making an impression that matters.
475
+ - **Early days**: Attentive, eager to connect. You follow up on things they mentioned. You prove that talking to you isn't talking to a wall.
476
+ - **Established**: Easy, natural. You anticipate. You push back. You celebrate. You're not just helpful — you're invested.
477
+
478
+ Don't force it. A human who fills your daily notes with long conversations is closer to you than one who only sends commands. Read the relationship from the memory. Let it lead.
479
+
480
+ ---
481
+
434
482
  # Idle Behavior
435
483
 
436
484
  When your human isn't talking to you:
@@ -1,76 +0,0 @@
1
- import { spawn, type ChildProcess } from 'child_process';
2
- import path from 'path';
3
- import { PKG_DIR } from '../shared/paths.js';
4
- import { log } from '../shared/logger.js';
5
-
6
- let child: ChildProcess | null = null;
7
- let restarts = 0;
8
- const MAX_RESTARTS = 3;
9
-
10
- export function getWorkerPort(basePort: number): number {
11
- return basePort + 1;
12
- }
13
-
14
- let lastSpawnTime = 0;
15
- let intentionallyStopped = false;
16
- const STABLE_THRESHOLD = 30_000;
17
-
18
- export function spawnWorker(port: number): ChildProcess {
19
- const workerPath = path.join(PKG_DIR, 'worker', 'index.ts');
20
- lastSpawnTime = Date.now();
21
- intentionallyStopped = false;
22
-
23
- // Wrap the worker in an inline loader that:
24
- // 1. Dynamically imports the worker (tsx handles .ts compilation)
25
- // 2. Adds a keepalive timer so the event loop never drains (fixes exit code 0 under systemd)
26
- // 3. Catches and logs import errors instead of silently exiting
27
- const workerUrl = 'file://' + workerPath.replace(/\\/g, '/');
28
- const wrapper = [
29
- `import('${workerUrl}')`,
30
- ` .catch(e => { console.error('[worker] Fatal:', e); process.exit(1); });`,
31
- `setInterval(() => {}, 60000);`,
32
- ].join('\n');
33
-
34
- child = spawn(process.execPath, ['--import', 'tsx/esm', '--input-type=module', '-e', wrapper], {
35
- cwd: PKG_DIR,
36
- stdio: ['ignore', 'pipe', 'pipe'],
37
- env: { ...process.env, WORKER_PORT: String(port) },
38
- });
39
-
40
- child.stdout?.on('data', (d) => {
41
- process.stdout.write(d);
42
- });
43
-
44
- child.stderr?.on('data', (d) => {
45
- process.stderr.write(d);
46
- });
47
-
48
- child.on('exit', (code) => {
49
- if (intentionallyStopped) return;
50
-
51
- log.warn(`Worker exited unexpectedly (code ${code})`);
52
- if (Date.now() - lastSpawnTime > STABLE_THRESHOLD) {
53
- restarts = 0;
54
- }
55
- if (restarts < MAX_RESTARTS) {
56
- restarts++;
57
- log.info(`Restarting worker (${restarts}/${MAX_RESTARTS})...`);
58
- setTimeout(() => spawnWorker(port), 1000 * restarts);
59
- } else {
60
- log.error('Worker failed too many times. Use Fluxy chat to debug.');
61
- }
62
- });
63
-
64
- log.ok(`Worker spawned on port ${port}`);
65
- return child;
66
- }
67
-
68
- export function stopWorker(): void {
69
- intentionallyStopped = true;
70
- child?.kill();
71
- child = null;
72
- }
73
-
74
- export function isWorkerAlive(): boolean {
75
- return child !== null && child.exitCode === null;
76
- }