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 +7 -4
- package/package.json +1 -1
- package/scripts/install +7 -4
- package/scripts/install.ps1 +14 -8
- package/scripts/install.sh +7 -4
- package/supervisor/backend.ts +103 -1
- package/supervisor/index.ts +39 -20
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}
|
|
121
|
-
${c.blue}${c.bold}
|
|
122
|
-
${c.
|
|
123
|
-
${c.
|
|
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
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}
|
|
29
|
-
printf "${BLUE}${BOLD}
|
|
30
|
-
printf "${
|
|
31
|
-
printf "${
|
|
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"
|
package/scripts/install.ps1
CHANGED
|
@@ -38,15 +38,21 @@ function Write-Down($text) {
|
|
|
38
38
|
|
|
39
39
|
Write-Host ""
|
|
40
40
|
if ($vtSupported) {
|
|
41
|
-
Write-Host "${BLUE}${BOLD}
|
|
42
|
-
Write-Host "${BLUE}${BOLD}
|
|
43
|
-
Write-Host "${
|
|
44
|
-
Write-Host "${
|
|
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 "
|
|
47
|
-
Write-Host "
|
|
48
|
-
Write-Host "
|
|
49
|
-
Write-Host "
|
|
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
|
package/scripts/install.sh
CHANGED
|
@@ -25,10 +25,13 @@ BOLD='\033[1m'
|
|
|
25
25
|
RESET='\033[0m'
|
|
26
26
|
|
|
27
27
|
printf "\n"
|
|
28
|
-
printf "${BLUE}${BOLD}
|
|
29
|
-
printf "${BLUE}${BOLD}
|
|
30
|
-
printf "${
|
|
31
|
-
printf "${
|
|
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"
|
package/supervisor/backend.ts
CHANGED
|
@@ -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(() =>
|
|
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
|
+
}
|
package/supervisor/index.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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...');
|