# v0.4.29 (2026-05-10)

## Features
- Add Cline & Kilo Code tool cards
- Tailscale TUN mode for stable Funnel TLS
- Sort APIKEY providers by usage, collapse to top 20

## Improvements
- Local Material Symbols font (no Google Fonts)
- Docker base: Bun → Node 22-alpine
- MITM reads aliases from JSON cache (no native sqlite)
- Stream stall timeout (2 min) in open-sse

## Fixes
- Fal.ai key test: use stable models endpoint
This commit is contained in:
decolua
2026-05-10 21:56:40 +07:00
parent 52c38cf94c
commit 7ad538bcf2
41 changed files with 1171 additions and 391 deletions
+57 -45
View File
@@ -433,8 +433,22 @@ async function ensureUserOwnedDir(dir) {
} catch { /* ignore */ }
}
/** Start tailscaled in userspace-networking mode (no root, no sudo prompt). */
export async function startDaemonWithPassword(_sudoPasswordUnused) {
/** Check if running daemon uses TUN mode (Funnel TLS requires TUN). */
function isDaemonTunMode() {
try {
const ps = execSync(`pgrep -af "tailscaled.*${TAILSCALE_SOCKET}"`, { encoding: "utf8", timeout: 2000 }).trim();
if (!ps) return null;
return !ps.includes("--tun=userspace-networking");
} catch { return null; }
}
/**
* Start tailscaled.
* - With sudoPassword: TUN mode (root) → Funnel TLS works
* - Without: userspace-networking fallback (no sudo, but Funnel TLS unstable)
* State always lives in ~/.9router/tailscale/ via --statedir.
*/
export async function startDaemonWithPassword(sudoPassword) {
if (IS_WINDOWS) {
// Windows: tailscale runs as a Windows Service. Start it then poll BackendState
// until daemon finishes init (avoids "NoState" errors when calling funnel/up too early).
@@ -459,64 +473,62 @@ export async function startDaemonWithPassword(_sudoPasswordUnused) {
return;
}
// Detect unhealthy state: dir/files not owned by current user OR multiple daemons running.
// Either condition blocks userspace daemon → must kill all + reclaim ownership.
let needsRestart = false;
try {
const st = fs.statSync(TAILSCALE_DIR);
if (st.uid !== process.getuid()) needsRestart = true;
// Also check state file (the actual unhealthy resource)
const stateFile = path.join(TAILSCALE_DIR, "tailscaled.state");
if (fs.existsSync(stateFile) && fs.statSync(stateFile).uid !== process.getuid()) needsRestart = true;
} catch { /* dir doesn't exist yet */ }
const wantTun = !!sudoPassword;
const currentMode = isDaemonTunMode(); // true=TUN, false=userspace, null=not running
// Detect duplicate daemons on same socket → also requires restart
if (!needsRestart) {
try {
const ps = execSync(`pgrep -f "tailscaled.*${TAILSCALE_SOCKET}"`, { encoding: "utf8", timeout: 2000 }).trim();
if (ps && ps.split("\n").length > 1) needsRestart = true;
} catch { /* no match → ok */ }
}
if (needsRestart) {
// Kill ALL tailscaled processes (root + user duplicates). Best-effort with/without sudo.
try { execSync("pkill -9 -x tailscaled", { stdio: "ignore", timeout: 3000 }); } catch { /* ignore */ }
try { execSync("sudo -n pkill -9 -x tailscaled", { stdio: "ignore", timeout: 3000 }); } catch { /* ignore */ }
await new Promise((r) => setTimeout(r, 1500));
} else {
// Check if our userspace daemon already responds
// Daemon already running in correct mode → reuse
if (currentMode !== null && currentMode === wantTun) {
try {
const bin = getTailscaleBin() || "tailscale";
execSync(`"${bin}" ${SOCKET_FLAG.join(" ")} status --json`, {
stdio: "ignore",
windowsHide: true,
env: { ...process.env, PATH: EXTENDED_PATH },
timeout: 3000
stdio: "ignore", windowsHide: true,
env: { ...process.env, PATH: EXTENDED_PATH }, timeout: 3000
});
return; // Already running and user-owned
} catch { /* not running, start it */ }
return;
} catch { /* unresponsive, restart below */ }
}
// Reclaim folder ownership if a previous root daemon left it locked
// Mode mismatch or unresponsive → kill all daemons on our socket
try { execSync(`pkill -9 -f "tailscaled.*${TAILSCALE_SOCKET}"`, { stdio: "ignore", timeout: 3000 }); } catch { /* ignore */ }
if (sudoPassword) {
try { await execWithPassword(`pkill -9 -f "tailscaled.*${TAILSCALE_SOCKET}"`, sudoPassword); } catch { /* ignore */ }
} else {
try { execSync(`sudo -n pkill -9 -f "tailscaled.*${TAILSCALE_SOCKET}"`, { stdio: "ignore", timeout: 3000 }); } catch { /* ignore */ }
}
await new Promise((r) => setTimeout(r, 1500));
// Reclaim folder ownership (previous root daemon may have locked it)
await ensureUserOwnedDir(TAILSCALE_DIR);
// Userspace-networking mode: no TUN device → no root needed → no sudo prompt
const tailscaledBin = IS_MAC ? "/usr/local/bin/tailscaled" : "tailscaled";
const args = [
const daemonArgs = [
`--socket=${TAILSCALE_SOCKET}`,
`--statedir=${TAILSCALE_DIR}`,
"--tun=userspace-networking",
];
if (!wantTun) daemonArgs.push("--tun=userspace-networking");
const child = spawn(tailscaledBin, args, {
detached: true,
stdio: "ignore",
cwd: os.tmpdir(),
env: { ...process.env, PATH: EXTENDED_PATH },
});
child.unref();
if (wantTun) {
// TUN mode: spawn via sudo, password via stdin. Detached so it survives parent exit.
const child = spawn("sudo", ["-S", tailscaledBin, ...daemonArgs], {
detached: true,
stdio: ["pipe", "ignore", "ignore"],
cwd: os.tmpdir(),
env: { ...process.env, PATH: EXTENDED_PATH },
});
child.stdin.write(`${sudoPassword}\n`);
child.stdin.end();
child.unref();
} else {
const child = spawn(tailscaledBin, daemonArgs, {
detached: true,
stdio: "ignore",
cwd: os.tmpdir(),
env: { ...process.env, PATH: EXTENDED_PATH },
});
child.unref();
}
// Wait for daemon socket to be ready
// Wait for socket ready
await new Promise((r) => setTimeout(r, 3000));
}