fluxy-bot 0.4.13 → 0.4.15

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
@@ -117,10 +117,13 @@ class Stepper {
117
117
 
118
118
  function banner() {
119
119
  console.log(`
120
- ${c.blue}${c.bold} ___ __ ${c.reset}
121
- ${c.blue}${c.bold} / _/ / / __ __ __ __ __ ${c.reset}
122
- ${c.pink}${c.bold} / _/ / / / / / / \\ \\/ / / / ${c.reset}
123
- ${c.pink}${c.bold} /_/ /_/ \\_,_/ /_/\\_\\ /_/ ${c.reset}
120
+ ${c.blue}${c.bold} _______ _ ${c.reset}
121
+ ${c.blue}${c.bold} (_______) | ${c.reset}
122
+ ${c.blue}${c.bold} _____ | |_ _ _ _ _ _ ${c.reset}
123
+ ${c.blue}${c.bold} | ___) | | | | ( \\ / ) | | | ${c.reset}
124
+ ${c.pink}${c.bold} | | | | |_| |) X (| |_| | ${c.reset}
125
+ ${c.pink}${c.bold} |_| |_|\\____(_/ \\_)\\__ | ${c.reset}
126
+ ${c.pink}${c.bold} (____/ ${c.reset}
124
127
  ${c.dim}v${pkg.version} · Self-hosted AI agent${c.reset}`);
125
128
  }
126
129
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluxy-bot",
3
- "version": "0.4.13",
3
+ "version": "0.4.15",
4
4
  "description": "Self-hosted, self-evolving AI agent with its own dashboard.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/scripts/install CHANGED
@@ -25,10 +25,13 @@ BOLD='\033[1m'
25
25
  RESET='\033[0m'
26
26
 
27
27
  printf "\n"
28
- printf "${BLUE}${BOLD} ___ __ ${RESET}\n"
29
- printf "${BLUE}${BOLD} / _/ / / __ __ __ __ __ ${RESET}\n"
30
- printf "${PINK}${BOLD} / _/ / / / / / / \\ \\/ / / / ${RESET}\n"
31
- printf "${PINK}${BOLD} /_/ /_/ \\_,_/ /_/\\_\\ /_/ ${RESET}\n"
28
+ printf "${BLUE}${BOLD} _______ _ ${RESET}\n"
29
+ printf "${BLUE}${BOLD} (_______) | ${RESET}\n"
30
+ printf "${BLUE}${BOLD} _____ | |_ _ _ _ _ _ ${RESET}\n"
31
+ printf "${BLUE}${BOLD} | ___) | | | | ( \\ / ) | | | ${RESET}\n"
32
+ printf "${PINK}${BOLD} | | | | |_| |) X (| |_| | ${RESET}\n"
33
+ printf "${PINK}${BOLD} |_| |_|\\____(_/ \\_)\\__ | ${RESET}\n"
34
+ printf "${PINK}${BOLD} (____/ ${RESET}\n"
32
35
  printf "\n"
33
36
  printf "${DIM} Self-hosted, self-evolving AI agent with its own dashboard.${RESET}\n"
34
37
  printf "${DIM} ─────────────────────────────${RESET}\n\n"
@@ -38,15 +38,21 @@ function Write-Down($text) {
38
38
 
39
39
  Write-Host ""
40
40
  if ($vtSupported) {
41
- Write-Host "${BLUE}${BOLD} ___ __ ${RSET}"
42
- Write-Host "${BLUE}${BOLD} / _/ / / __ __ __ __ __ ${RSET}"
43
- Write-Host "${PINK}${BOLD} / _/ / / / / / / \ \/ / / / ${RSET}"
44
- Write-Host "${PINK}${BOLD} /_/ /_/ \_,_/ /_/\_\ /_/ ${RSET}"
41
+ Write-Host "${BLUE}${BOLD} _______ _ ${RSET}"
42
+ Write-Host "${BLUE}${BOLD} (_______) | ${RSET}"
43
+ Write-Host "${BLUE}${BOLD} _____ | |_ _ _ _ _ _ ${RSET}"
44
+ Write-Host "${BLUE}${BOLD} | ___) | | | | ( \ / ) | | | ${RSET}"
45
+ Write-Host "${PINK}${BOLD} | | | | |_| |) X (| |_| | ${RSET}"
46
+ Write-Host "${PINK}${BOLD} |_| |_|\____(_/ \_)\__ | ${RSET}"
47
+ Write-Host "${PINK}${BOLD} (____/ ${RSET}"
45
48
  } else {
46
- Write-Host " ___ __ " -ForegroundColor Cyan
47
- Write-Host " / _/ / / __ __ __ __ __ " -ForegroundColor Cyan
48
- Write-Host " / _/ / / / / / / \ \/ / / / " -ForegroundColor Magenta
49
- Write-Host " /_/ /_/ \_,_/ /_/\_\ /_/ " -ForegroundColor Magenta
49
+ Write-Host " _______ _ " -ForegroundColor Cyan
50
+ Write-Host " (_______) | " -ForegroundColor Cyan
51
+ Write-Host " _____ | |_ _ _ _ _ _ " -ForegroundColor Cyan
52
+ Write-Host " | ___) | | | | ( \ / ) | | | " -ForegroundColor Cyan
53
+ Write-Host " | | | | |_| |) X (| |_| | " -ForegroundColor Magenta
54
+ Write-Host " |_| |_|\____(_/ \_)\__ | " -ForegroundColor Magenta
55
+ Write-Host " (____/ " -ForegroundColor Magenta
50
56
  }
51
57
  Write-Host ""
52
58
  Write-Host " Self-hosted, self-evolving AI agent with its own dashboard." -ForegroundColor DarkGray
@@ -25,10 +25,13 @@ BOLD='\033[1m'
25
25
  RESET='\033[0m'
26
26
 
27
27
  printf "\n"
28
- printf "${BLUE}${BOLD} ___ __ ${RESET}\n"
29
- printf "${BLUE}${BOLD} / _/ / / __ __ __ __ __ ${RESET}\n"
30
- printf "${PINK}${BOLD} / _/ / / / / / / \\ \\/ / / / ${RESET}\n"
31
- printf "${PINK}${BOLD} /_/ /_/ \\_,_/ /_/\\_\\ /_/ ${RESET}\n"
28
+ printf "${BLUE}${BOLD} _______ _ ${RESET}\n"
29
+ printf "${BLUE}${BOLD} (_______) | ${RESET}\n"
30
+ printf "${BLUE}${BOLD} _____ | |_ _ _ _ _ _ ${RESET}\n"
31
+ printf "${BLUE}${BOLD} | ___) | | | | ( \\ / ) | | | ${RESET}\n"
32
+ printf "${PINK}${BOLD} | | | | |_| |) X (| |_| | ${RESET}\n"
33
+ printf "${PINK}${BOLD} |_| |_|\\____(_/ \\_)\\__ | ${RESET}\n"
34
+ printf "${PINK}${BOLD} (____/ ${RESET}\n"
32
35
  printf "\n"
33
36
  printf "${DIM} Self-hosted, self-evolving AI agent with its own dashboard.${RESET}\n"
34
37
  printf "${DIM} ─────────────────────────────${RESET}\n\n"
@@ -1,11 +1,24 @@
1
1
  import { spawn, type ChildProcess } from 'child_process';
2
+ import fs from 'fs';
2
3
  import path from 'path';
3
4
  import { PKG_DIR } from '../shared/paths.js';
4
5
  import { log } from '../shared/logger.js';
5
6
 
6
7
  let child: ChildProcess | null = null;
7
8
  let restarts = 0;
9
+ let lastSpawnTime = 0;
10
+ let pendingRestartTimer: ReturnType<typeof setTimeout> | null = null;
8
11
  const MAX_RESTARTS = 3;
12
+ const STABLE_THRESHOLD_MS = 30_000; // 30s uptime = reset counter
13
+
14
+ // File watcher state
15
+ let watcher: fs.FSWatcher | null = null;
16
+ let restartInProgress = false;
17
+ let restartInProgressTimer: ReturnType<typeof setTimeout> | null = null;
18
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
19
+
20
+ const IGNORED_EXTENSIONS = new Set(['.db', '.sqlite', '.db-journal', '.db-wal']);
21
+ const IGNORED_DIRS = new Set(['node_modules', '.git']);
9
22
 
10
23
  export function getBackendPort(basePort: number): number {
11
24
  return basePort + 4;
@@ -20,6 +33,8 @@ export function spawnBackend(port: number): ChildProcess {
20
33
  env: { ...process.env, BACKEND_PORT: String(port) },
21
34
  });
22
35
 
36
+ lastSpawnTime = Date.now();
37
+
23
38
  child.stdout?.on('data', (d) => {
24
39
  process.stdout.write(d);
25
40
  });
@@ -31,10 +46,20 @@ export function spawnBackend(port: number): ChildProcess {
31
46
  child.on('exit', (code) => {
32
47
  if (code !== 0 && code !== null) {
33
48
  log.warn(`Backend crashed (code ${code})`);
49
+
50
+ // Auto-reset counter if the process was stable (alive >30s)
51
+ const uptime = Date.now() - lastSpawnTime;
52
+ if (uptime > STABLE_THRESHOLD_MS) {
53
+ restarts = 0;
54
+ }
55
+
34
56
  if (restarts < MAX_RESTARTS) {
35
57
  restarts++;
36
58
  log.info(`Restarting backend (${restarts}/${MAX_RESTARTS})...`);
37
- setTimeout(() => spawnBackend(port), 1000);
59
+ pendingRestartTimer = setTimeout(() => {
60
+ pendingRestartTimer = null;
61
+ spawnBackend(port);
62
+ }, 1000);
38
63
  } else {
39
64
  log.error('Backend failed too many times. Use Fluxy chat to debug.');
40
65
  }
@@ -46,6 +71,11 @@ export function spawnBackend(port: number): ChildProcess {
46
71
  }
47
72
 
48
73
  export function stopBackend(): void {
74
+ // Clear any pending crash-recovery restart
75
+ if (pendingRestartTimer) {
76
+ clearTimeout(pendingRestartTimer);
77
+ pendingRestartTimer = null;
78
+ }
49
79
  child?.kill();
50
80
  child = null;
51
81
  }
@@ -57,3 +87,75 @@ export function isBackendAlive(): boolean {
57
87
  export function resetBackendRestarts(): void {
58
88
  restarts = 0;
59
89
  }
90
+
91
+ /**
92
+ * Mark that a restart is already in progress (e.g. from Fluxy agent bot:done).
93
+ * The file watcher will skip triggering a restart for the next 2 seconds.
94
+ */
95
+ export function markRestartInProgress(): void {
96
+ restartInProgress = true;
97
+ if (restartInProgressTimer) clearTimeout(restartInProgressTimer);
98
+ restartInProgressTimer = setTimeout(() => {
99
+ restartInProgress = false;
100
+ restartInProgressTimer = null;
101
+ }, 2000);
102
+ }
103
+
104
+ /**
105
+ * Watch workspace/backend/ for file changes and auto-restart.
106
+ * Returns a cleanup function.
107
+ */
108
+ export function startBackendWatcher(port: number, onRestart?: () => void): void {
109
+ const watchDir = path.join(PKG_DIR, 'workspace', 'backend');
110
+
111
+ try {
112
+ watcher = fs.watch(watchDir, { recursive: true }, (_event, filename) => {
113
+ if (!filename) return;
114
+
115
+ // Filter out ignored files
116
+ const ext = path.extname(filename);
117
+ if (IGNORED_EXTENSIONS.has(ext)) return;
118
+
119
+ // Filter out ignored directories
120
+ const parts = filename.split(path.sep);
121
+ if (parts.some((p) => IGNORED_DIRS.has(p))) return;
122
+
123
+ // Debounce rapid saves (500ms)
124
+ if (debounceTimer) clearTimeout(debounceTimer);
125
+ debounceTimer = setTimeout(() => {
126
+ debounceTimer = null;
127
+
128
+ // Skip if a restart was already triggered (e.g. by Fluxy agent)
129
+ if (restartInProgress) {
130
+ log.info(`[watcher] File changed (${filename}) — restart already in progress, skipping`);
131
+ return;
132
+ }
133
+
134
+ log.info(`[watcher] File changed: ${filename} — restarting backend`);
135
+ resetBackendRestarts();
136
+ stopBackend();
137
+ spawnBackend(port);
138
+ onRestart?.();
139
+ }, 500);
140
+ });
141
+
142
+ log.ok(`Watching workspace/backend/ for changes`);
143
+ } catch (err) {
144
+ log.warn(`File watcher failed: ${err instanceof Error ? err.message : err}`);
145
+ }
146
+ }
147
+
148
+ export function stopBackendWatcher(): void {
149
+ if (watcher) {
150
+ watcher.close();
151
+ watcher = null;
152
+ }
153
+ if (debounceTimer) {
154
+ clearTimeout(debounceTimer);
155
+ debounceTimer = null;
156
+ }
157
+ if (restartInProgressTimer) {
158
+ clearTimeout(restartInProgressTimer);
159
+ restartInProgressTimer = null;
160
+ }
161
+ }
@@ -10,7 +10,7 @@ import { PKG_DIR } from '../shared/paths.js';
10
10
  import { log } from '../shared/logger.js';
11
11
  import { startTunnel, stopTunnel, isTunnelAlive, restartTunnel } from './tunnel.js';
12
12
  import { spawnWorker, stopWorker, getWorkerPort, isWorkerAlive } from './worker.js';
13
- import { spawnBackend, stopBackend, getBackendPort, isBackendAlive, resetBackendRestarts } from './backend.js';
13
+ import { spawnBackend, stopBackend, getBackendPort, resetBackendRestarts, startBackendWatcher, stopBackendWatcher, markRestartInProgress } from './backend.js';
14
14
  import { updateTunnelUrl, startHeartbeat, stopHeartbeat, disconnect } from '../shared/relay.js';
15
15
  import { startFluxyAgentQuery, stopFluxyAgentQuery, clearFluxySession } from './fluxy-agent.js';
16
16
  import { ensureFileDirs, saveAttachment, type SavedFile } from './file-saver.js';
@@ -162,30 +162,44 @@ export async function startSupervisor() {
162
162
  return;
163
163
  }
164
164
 
165
- // App API routes → proxy to user's backend server
165
+ // App API routes → proxy to user's backend server (with retry on startup)
166
166
  if (req.url?.startsWith('/app/api')) {
167
167
  const backendPath = req.url.replace(/^\/app\/api/, '') || '/';
168
168
  console.log(`[supervisor] → backend :${backendPort} | ${req.method} ${backendPath}`);
169
- if (!isBackendAlive()) {
170
- console.log('[supervisor] Backend down — returning 503');
171
- res.writeHead(503, { 'Content-Type': 'application/json' });
172
- res.end(JSON.stringify({ error: 'Backend is starting...' }));
173
- return;
174
- }
175
169
 
176
- const proxy = http.request(
177
- { host: '127.0.0.1', port: backendPort, path: backendPath, method: req.method, headers: req.headers },
178
- (proxyRes) => {
179
- res.writeHead(proxyRes.statusCode!, proxyRes.headers);
180
- proxyRes.pipe(res);
181
- },
182
- );
183
- proxy.on('error', (e) => {
184
- console.error(`[supervisor] Backend proxy error: ${req.url}`, e.message);
185
- res.writeHead(503, { 'Content-Type': 'application/json' });
186
- res.end(JSON.stringify({ error: 'Backend unavailable' }));
170
+ // Buffer request body so we can replay it on retry
171
+ const chunks: Buffer[] = [];
172
+ req.on('data', (chunk) => chunks.push(chunk));
173
+ req.on('end', () => {
174
+ const body = Buffer.concat(chunks);
175
+ let attempt = 0;
176
+ const MAX_RETRIES = 3;
177
+ const RETRY_DELAY = 500;
178
+
179
+ function tryProxy() {
180
+ const proxy = http.request(
181
+ { host: '127.0.0.1', port: backendPort, path: backendPath, method: req.method, headers: req.headers },
182
+ (proxyRes) => {
183
+ res.writeHead(proxyRes.statusCode!, proxyRes.headers);
184
+ proxyRes.pipe(res);
185
+ },
186
+ );
187
+ proxy.on('error', (e: NodeJS.ErrnoException) => {
188
+ if (e.code === 'ECONNREFUSED' && attempt < MAX_RETRIES) {
189
+ attempt++;
190
+ console.log(`[supervisor] Backend not ready, retry ${attempt}/${MAX_RETRIES}...`);
191
+ setTimeout(tryProxy, RETRY_DELAY);
192
+ } else {
193
+ console.error(`[supervisor] Backend proxy error: ${req.url}`, e.message);
194
+ res.writeHead(503, { 'Content-Type': 'application/json' });
195
+ res.end(JSON.stringify({ error: 'Backend unavailable' }));
196
+ }
197
+ });
198
+ proxy.end(body);
199
+ }
200
+
201
+ tryProxy();
187
202
  });
188
- req.pipe(proxy);
189
203
  return;
190
204
  }
191
205
 
@@ -423,6 +437,7 @@ export async function startSupervisor() {
423
437
  if (eventData.usedFileTools) {
424
438
  console.log('[supervisor] File tools used — Vite HMR will apply changes automatically');
425
439
  console.log('[supervisor] Restarting backend...');
440
+ markRestartInProgress(); // prevent file watcher from double-restarting
426
441
  resetBackendRestarts();
427
442
  stopBackend();
428
443
  spawnBackend(backendPort);
@@ -564,6 +579,9 @@ export async function startSupervisor() {
564
579
  // Spawn worker + backend
565
580
  spawnWorker(workerPort);
566
581
  spawnBackend(backendPort);
582
+ startBackendWatcher(backendPort, () => {
583
+ broadcastFluxy('app:hmr-update');
584
+ });
567
585
 
568
586
  // Tunnel
569
587
  let tunnelUrl: string | null = null;
@@ -643,6 +661,7 @@ export async function startSupervisor() {
643
661
  delete latestConfig.tunnelUrl;
644
662
  saveConfig(latestConfig);
645
663
  stopWorker();
664
+ stopBackendWatcher();
646
665
  stopBackend();
647
666
  stopTunnel();
648
667
  console.log('[supervisor] Stopping Vite dev servers...');