svamp-cli 0.2.317 → 0.2.319

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.
Files changed (28) hide show
  1. package/dist/{adminCommands-fJVu-h9l.mjs → adminCommands-DBADX_Uo.mjs} +1 -1
  2. package/dist/{agentCommands-DdOSpUEW.mjs → agentCommands-BJeGwkeX.mjs} +6 -6
  3. package/dist/{auth-B2XK13r-.mjs → auth-0NUGwOc0.mjs} +1 -1
  4. package/dist/{cli-DGVQaDbd.mjs → cli-CWElpiIq.mjs} +76 -76
  5. package/dist/cli.mjs +2 -2
  6. package/dist/{commands-Bx2JkNft.mjs → commands-AdVHZXK6.mjs} +1 -1
  7. package/dist/{commands-wQYdc5EK.mjs → commands-BY1lmXaM.mjs} +1 -1
  8. package/dist/{commands-DEsDSuMW.mjs → commands-BsaTwKnd.mjs} +2 -2
  9. package/dist/{commands-CoBMjrtP.mjs → commands-C0lwV0U3.mjs} +2 -2
  10. package/dist/{commands-C4p4v9eO.mjs → commands-CJWgDDl5.mjs} +2 -2
  11. package/dist/{commands-DUdStLKb.mjs → commands-CLgdrTSU.mjs} +2 -2
  12. package/dist/{commands-ino-Gtcu.mjs → commands-DMSkWb0h.mjs} +8 -8
  13. package/dist/{commands-I9jHLkrI.mjs → commands-DubEbPAY.mjs} +1 -1
  14. package/dist/{fleet-NRi2YMso.mjs → fleet-OKnU0cYK.mjs} +2 -2
  15. package/dist/{headlessCli-DpTQ00fL.mjs → headlessCli-CCO0IFYl.mjs} +2 -2
  16. package/dist/index.mjs +1 -1
  17. package/dist/{notifyCommands-BFRuzxXP.mjs → notifyCommands-4GUy4Mcl.mjs} +1 -1
  18. package/dist/{package-DN_Oa_OT.mjs → package-BEEqsQhN.mjs} +3 -3
  19. package/dist/{pinnedClaudeCode-CnCpensV.mjs → pinnedClaudeCode-dbXgsGNK.mjs} +1 -1
  20. package/dist/{rpc-CclCeDcu.mjs → rpc-FGp88Yxp.mjs} +1 -1
  21. package/dist/{rpc-Cx6KsBYC.mjs → rpc-TXF-di9u.mjs} +1 -1
  22. package/dist/{run-RA4Vr6lj.mjs → run-09AkgeGM.mjs} +1134 -264
  23. package/dist/{run-DKhnAl2f.mjs → run-7TVWXnOM.mjs} +1 -1
  24. package/dist/{scheduler-Wzzed1CU.mjs → scheduler-b-ysyKIj.mjs} +1 -1
  25. package/dist/{serveCommands-DQeSesYI.mjs → serveCommands-D3W7w0G6.mjs} +29 -7
  26. package/dist/{sideband-gQ7oQEsx.mjs → sideband-C1db5sMF.mjs} +1 -1
  27. package/package.json +3 -3
  28. package/dist/frpc-CNZJM3qC.mjs +0 -775
@@ -1,20 +1,21 @@
1
- import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import os$1, { homedir } from 'os';
1
+ import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import os$1, { homedir, platform, arch } from 'os';
2
2
  import fs$1, { mkdir as mkdir$1, readdir as readdir$1, readFile, writeFile as writeFile$1, rename, unlink } from 'fs/promises';
3
3
  import * as fs from 'fs';
4
- import fs__default, { existsSync as existsSync$1, rmSync, readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync as renameSync$1, realpathSync as realpathSync$1, copyFileSync, unlinkSync as unlinkSync$1, readdirSync as readdirSync$1, watch, rmdirSync, statSync as statSync$1 } from 'fs';
4
+ import fs__default, { existsSync as existsSync$1, rmSync, readFileSync as readFileSync$1, mkdirSync, writeFileSync, chmodSync, unlinkSync, renameSync as renameSync$1, realpathSync as realpathSync$1, copyFileSync, readdirSync as readdirSync$1, watch, rmdirSync, statSync as statSync$1 } from 'fs';
5
5
  import * as path from 'path';
6
6
  import path__default, { join, delimiter, resolve as resolve$1, dirname as dirname$1, basename as basename$1 } from 'path';
7
7
  import { fileURLToPath } from 'url';
8
- import { execFile, spawn, execSync, exec as exec$1 } from 'child_process';
8
+ import { execFile, execSync, spawn, exec as exec$1 } from 'child_process';
9
9
  import * as crypto from 'crypto';
10
- import { randomUUID as randomUUID$1, createHash as createHash$1 } from 'crypto';
11
- import { randomUUID, randomBytes, createHash, timingSafeEqual } from 'node:crypto';
12
- import { existsSync, readFileSync, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1, chmodSync, rmSync as rmSync$1, cpSync, statSync, realpathSync, readdirSync, renameSync, appendFileSync, openSync, readSync, closeSync, unlinkSync } from 'node:fs';
10
+ import { randomUUID, createHash } from 'crypto';
11
+ import { randomUUID as randomUUID$1, randomBytes, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
12
+ import { existsSync, readFileSync, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1, chmodSync as chmodSync$1, rmSync as rmSync$1, cpSync, statSync, realpathSync, readdirSync, renameSync, appendFileSync, openSync, readSync, closeSync, unlinkSync as unlinkSync$1 } from 'node:fs';
13
13
  import { exec, spawn as spawn$1, execSync as execSync$1, execFile as execFile$1, execFileSync } from 'node:child_process';
14
14
  import { promisify } from 'util';
15
15
  import * as http from 'http';
16
16
  import * as net from 'net';
17
- import os, { homedir as homedir$1, platform } from 'node:os';
17
+ import { createServer } from 'net';
18
+ import os, { homedir as homedir$1, platform as platform$1 } from 'node:os';
18
19
  import { join as join$1, extname, resolve, sep, basename, dirname } from 'node:path';
19
20
  import { EventEmitter } from 'node:events';
20
21
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
@@ -1260,6 +1261,1027 @@ async function killDescendant(rootPid, targetPid, signal = "SIGTERM") {
1260
1261
  }
1261
1262
  }
1262
1263
 
1264
+ function svampHome() {
1265
+ return process.env.SVAMP_HOME || join(homedir(), ".svamp");
1266
+ }
1267
+ function cacheFile() {
1268
+ return join(svampHome(), "instance-config.json");
1269
+ }
1270
+ const CONFIG_FILENAME = "svamp.json";
1271
+ let _config = null;
1272
+ let _loaded = false;
1273
+ let _cacheChecked = false;
1274
+ function stripSlash$1(u) {
1275
+ return u.replace(/\/+$/, "");
1276
+ }
1277
+ function getInstanceUrl() {
1278
+ const raw = (process.env.SVAMP_INSTANCE_URL || "").trim();
1279
+ return raw ? stripSlash$1(raw) : void 0;
1280
+ }
1281
+ function configUrlsFor(anchor) {
1282
+ const base = stripSlash$1(anchor);
1283
+ return [`${base}/${CONFIG_FILENAME}`, `${base}/.well-known/${CONFIG_FILENAME}`];
1284
+ }
1285
+ async function fetchConfig(anchor) {
1286
+ for (const url of configUrlsFor(anchor)) {
1287
+ try {
1288
+ const resp = await fetch(url, { signal: AbortSignal.timeout(8e3) });
1289
+ if (!resp.ok) continue;
1290
+ const data = await resp.json();
1291
+ if (data && typeof data === "object" && !Array.isArray(data)) {
1292
+ return data;
1293
+ }
1294
+ } catch {
1295
+ }
1296
+ }
1297
+ return null;
1298
+ }
1299
+ function readCache() {
1300
+ try {
1301
+ if (existsSync$1(cacheFile())) {
1302
+ const data = JSON.parse(readFileSync$1(cacheFile(), "utf-8"));
1303
+ if (data && typeof data === "object" && !Array.isArray(data)) return data;
1304
+ }
1305
+ } catch {
1306
+ }
1307
+ return null;
1308
+ }
1309
+ function writeCache(cfg) {
1310
+ try {
1311
+ mkdirSync(svampHome(), { recursive: true });
1312
+ writeFileSync(cacheFile(), JSON.stringify(cfg, null, 2) + "\n", "utf-8");
1313
+ } catch {
1314
+ }
1315
+ }
1316
+ function applyConfig(cfg) {
1317
+ _config = cfg;
1318
+ if (cfg.hyphaServerUrl && typeof cfg.hyphaServerUrl === "string") {
1319
+ process.env.HYPHA_SERVER_URL = stripSlash$1(cfg.hyphaServerUrl.trim());
1320
+ }
1321
+ }
1322
+ async function loadInstanceConfig(opts = {}) {
1323
+ if (_loaded && !opts.force) return _config;
1324
+ const anchor = opts.anchor ? stripSlash$1(opts.anchor) : getInstanceUrl();
1325
+ if (anchor) {
1326
+ const fetched = await fetchConfig(anchor);
1327
+ if (fetched) {
1328
+ applyConfig(fetched);
1329
+ writeCache(fetched);
1330
+ _loaded = true;
1331
+ _cacheChecked = true;
1332
+ return _config;
1333
+ }
1334
+ }
1335
+ const cached = readCache();
1336
+ if (cached) applyConfig(cached);
1337
+ _loaded = true;
1338
+ _cacheChecked = true;
1339
+ return _config;
1340
+ }
1341
+ function getLoadedConfig() {
1342
+ if (!_loaded && !_cacheChecked) {
1343
+ const cached = readCache();
1344
+ if (cached) applyConfig(cached);
1345
+ _cacheChecked = true;
1346
+ }
1347
+ return _config;
1348
+ }
1349
+ function clearInstanceConfigCache() {
1350
+ _config = null;
1351
+ _loaded = false;
1352
+ _cacheChecked = false;
1353
+ try {
1354
+ if (existsSync$1(cacheFile())) rmSync(cacheFile());
1355
+ } catch {
1356
+ }
1357
+ }
1358
+ function __setConfigForTest(cfg) {
1359
+ _config = cfg;
1360
+ _loaded = true;
1361
+ _cacheChecked = true;
1362
+ if (cfg) applyConfig(cfg);
1363
+ }
1364
+ function __resetForTest() {
1365
+ _config = null;
1366
+ _loaded = false;
1367
+ _cacheChecked = false;
1368
+ }
1369
+
1370
+ var instanceConfig = /*#__PURE__*/Object.freeze({
1371
+ __proto__: null,
1372
+ CONFIG_FILENAME: CONFIG_FILENAME,
1373
+ __resetForTest: __resetForTest,
1374
+ __setConfigForTest: __setConfigForTest,
1375
+ clearInstanceConfigCache: clearInstanceConfigCache,
1376
+ configUrlsFor: configUrlsFor,
1377
+ getInstanceUrl: getInstanceUrl,
1378
+ getLoadedConfig: getLoadedConfig,
1379
+ loadInstanceConfig: loadInstanceConfig
1380
+ });
1381
+
1382
+ function stripSlash(u) {
1383
+ return u.replace(/\/+$/, "");
1384
+ }
1385
+ function getHyphaServerUrl() {
1386
+ const fromCfg = getLoadedConfig()?.hyphaServerUrl;
1387
+ if (typeof fromCfg === "string" && fromCfg.trim()) return stripSlash(fromCfg.trim());
1388
+ const env = (process.env.HYPHA_SERVER_URL || "").trim();
1389
+ return env ? stripSlash(env) : void 0;
1390
+ }
1391
+ function getFrpsSubdomainHost() {
1392
+ const fromCfg = getLoadedConfig()?.frps?.subdomainHost;
1393
+ if (typeof fromCfg === "string" && fromCfg.trim()) return fromCfg.trim();
1394
+ const env = (process.env.FRPS_SUBDOMAIN_HOST || "").trim();
1395
+ if (env) return env;
1396
+ throw new Error(
1397
+ 'frps subdomain host is not configured. Serve a svamp.json with "frps.subdomainHost" at your frontend URL (svamp login <frontend-url>), or set FRPS_SUBDOMAIN_HOST (legacy). It is deployment-specific and never guessed.'
1398
+ );
1399
+ }
1400
+ function getFrpsServerAddr() {
1401
+ const fromCfg = getLoadedConfig()?.frps?.serverAddr;
1402
+ if (typeof fromCfg === "string" && fromCfg.trim()) return fromCfg.trim();
1403
+ const env = (process.env.FRPS_SERVER_ADDR || "").trim();
1404
+ if (env) return env;
1405
+ return `frps-ctrl.${getFrpsSubdomainHost()}`;
1406
+ }
1407
+ function getFrpsServerPort() {
1408
+ const fromCfg = getLoadedConfig()?.frps?.serverPort;
1409
+ if (typeof fromCfg === "number" && Number.isFinite(fromCfg)) return fromCfg;
1410
+ const env = parseInt(process.env.FRPS_SERVER_PORT || "", 10);
1411
+ return Number.isFinite(env) ? env : void 0;
1412
+ }
1413
+ function getSvampWebBaseUrl() {
1414
+ const fromCfg = getLoadedConfig()?.web?.baseUrl;
1415
+ if (typeof fromCfg === "string" && fromCfg.trim()) return stripSlash(fromCfg.trim());
1416
+ const env = (process.env.SVAMP_WEB_URL || "").trim();
1417
+ if (env) return stripSlash(env);
1418
+ const instance = getInstanceUrl();
1419
+ if (instance) return instance;
1420
+ throw new Error(
1421
+ 'Svamp web base URL is not configured. Serve a svamp.json with "web.baseUrl", log in with your frontend URL (svamp login <frontend-url>), or set SVAMP_WEB_URL (legacy).'
1422
+ );
1423
+ }
1424
+ function getSkillsServerUrl() {
1425
+ const fromCfg = getLoadedConfig()?.skills?.server;
1426
+ if (typeof fromCfg === "string" && fromCfg.trim()) return stripSlash(fromCfg.trim());
1427
+ const env = (process.env.HYPHA_SKILLS_SERVER || "").trim();
1428
+ if (env) return stripSlash(env);
1429
+ const server = getHyphaServerUrl();
1430
+ if (server) return server;
1431
+ throw new Error(
1432
+ "Cannot determine skills marketplace server. Serve a svamp.json or set HYPHA_SERVER_URL."
1433
+ );
1434
+ }
1435
+ function getSkillsWorkspace() {
1436
+ const fromCfg = getLoadedConfig()?.skills?.workspace;
1437
+ if (typeof fromCfg === "string" && fromCfg.trim()) return fromCfg.trim();
1438
+ return (process.env.HYPHA_SKILLS_WORKSPACE || "").trim() || "hypha-cloud";
1439
+ }
1440
+ function getSkillsCollection() {
1441
+ const fromCfg = getLoadedConfig()?.skills?.collection;
1442
+ if (typeof fromCfg === "string" && fromCfg.trim()) return fromCfg.trim();
1443
+ return (process.env.HYPHA_SKILLS_COLLECTION || "").trim() || "marketplace";
1444
+ }
1445
+
1446
+ function getMachineFingerprint() {
1447
+ const idFile = join(homedir(), ".svamp", "machine-id");
1448
+ try {
1449
+ const existing = readFileSync$1(idFile, "utf-8").trim();
1450
+ if (existing) return existing;
1451
+ } catch {
1452
+ }
1453
+ const id = randomUUID();
1454
+ try {
1455
+ mkdirSync(join(homedir(), ".svamp"), { recursive: true });
1456
+ writeFileSync(idFile, id + "\n");
1457
+ } catch {
1458
+ }
1459
+ return id;
1460
+ }
1461
+ const FRP_VERSION = "0.68.0";
1462
+ const BIN_DIR = join(homedir(), ".svamp", "bin");
1463
+ const FRPC_BIN = join(BIN_DIR, platform() === "win32" ? "frpc.exe" : "frpc");
1464
+ const _livingFrpcPids = /* @__PURE__ */ new Set();
1465
+ function isFrpcSubdomainConflict(errorLine) {
1466
+ return /router config conflict|already exists/i.test(errorLine);
1467
+ }
1468
+ function shouldGiveUpOnConflict(args) {
1469
+ if (args.everStartedProxy) return false;
1470
+ return args.conflictCount >= args.minAttempts && args.elapsedMs >= args.minElapsedMs;
1471
+ }
1472
+ function selectOrphanedFrpcPids(psOutput, selfPid, livingPids) {
1473
+ const pids = [];
1474
+ for (const line of psOutput.split("\n")) {
1475
+ const m = /^\s*(\d+)\s+(.*)$/.exec(line);
1476
+ if (!m) continue;
1477
+ const pid = Number(m[1]);
1478
+ const cmd = m[2];
1479
+ if (!cmd.includes(".svamp/bin/frpc")) continue;
1480
+ if (pid === selfPid || livingPids.has(pid)) continue;
1481
+ pids.push(pid);
1482
+ }
1483
+ return pids;
1484
+ }
1485
+ function killOrphanedFrpc(log) {
1486
+ if (process.platform === "win32") return;
1487
+ try {
1488
+ const out = execSync("ps -axo pid=,command=", { encoding: "utf-8" });
1489
+ const pids = selectOrphanedFrpcPids(out, process.pid, _livingFrpcPids);
1490
+ for (const pid of pids) {
1491
+ try {
1492
+ process.kill(pid, "SIGKILL");
1493
+ } catch {
1494
+ }
1495
+ }
1496
+ if (pids.length) {
1497
+ log(`Reaped ${pids.length} orphaned frpc process(es) from a prior daemon generation \u2014 they were retrying stale subdomains and causing frps router-config-conflict churn.`);
1498
+ }
1499
+ } catch {
1500
+ }
1501
+ }
1502
+ function isInCluster() {
1503
+ return !!(process.env.KUBERNETES_SERVICE_HOST || process.env.SANDBOX_ID);
1504
+ }
1505
+ const FRPS_CLUSTER_ADDR = "frps.hypha.svc.cluster.local";
1506
+ const DEFAULT_FRPS_PORT = isInCluster() ? 7e3 : 443;
1507
+ const FRPS_PUBLIC_TOKEN = "hypha-frps-public";
1508
+ function getFrpcDownloadUrl() {
1509
+ const os = platform();
1510
+ const a = arch();
1511
+ let osStr;
1512
+ let archStr;
1513
+ switch (os) {
1514
+ case "darwin":
1515
+ osStr = "darwin";
1516
+ break;
1517
+ case "linux":
1518
+ osStr = "linux";
1519
+ break;
1520
+ case "win32":
1521
+ osStr = "windows";
1522
+ break;
1523
+ default:
1524
+ throw new Error(`Unsupported platform: ${os}`);
1525
+ }
1526
+ switch (a) {
1527
+ case "x64":
1528
+ archStr = "amd64";
1529
+ break;
1530
+ case "arm64":
1531
+ archStr = "arm64";
1532
+ break;
1533
+ case "arm":
1534
+ archStr = "arm";
1535
+ break;
1536
+ default:
1537
+ throw new Error(`Unsupported architecture: ${a}`);
1538
+ }
1539
+ const ext = os === "win32" ? "zip" : "tar.gz";
1540
+ const filename = `frp_${FRP_VERSION}_${osStr}_${archStr}`;
1541
+ return `https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/${filename}.${ext}`;
1542
+ }
1543
+ let _frpcDownloadInFlight = null;
1544
+ async function ensureFrpc(log) {
1545
+ if (existsSync$1(FRPC_BIN)) {
1546
+ try {
1547
+ const out = execSync(`"${FRPC_BIN}" --version`, { stdio: "pipe", timeout: 5e3 }).toString().trim();
1548
+ if (out === FRP_VERSION) return FRPC_BIN;
1549
+ (log || console.log)(`frpc version mismatch: ${out} \u2260 ${FRP_VERSION}, re-downloading...`);
1550
+ } catch {
1551
+ (log || console.log)("frpc binary broken or invalid signature, re-downloading...");
1552
+ }
1553
+ }
1554
+ if (_frpcDownloadInFlight) return _frpcDownloadInFlight;
1555
+ _frpcDownloadInFlight = downloadFrpc(log).finally(() => {
1556
+ _frpcDownloadInFlight = null;
1557
+ });
1558
+ return _frpcDownloadInFlight;
1559
+ }
1560
+ async function downloadFrpc(log) {
1561
+ try {
1562
+ if (existsSync$1(FRPC_BIN)) unlinkSync(FRPC_BIN);
1563
+ } catch {
1564
+ }
1565
+ const logger = log || console.log;
1566
+ mkdirSync(BIN_DIR, { recursive: true });
1567
+ const url = getFrpcDownloadUrl();
1568
+ logger(`Downloading frpc ${FRP_VERSION} from ${url}...`);
1569
+ const ctrl = new AbortController();
1570
+ const timer = setTimeout(() => ctrl.abort(), 6e4);
1571
+ let response;
1572
+ try {
1573
+ response = await fetch(url, { signal: ctrl.signal });
1574
+ } catch (err) {
1575
+ const why = err?.name === "AbortError" ? "timed out after 60s" : err?.message ?? String(err);
1576
+ throw new Error(`Failed to download frpc from ${url}: ${why}`);
1577
+ } finally {
1578
+ clearTimeout(timer);
1579
+ }
1580
+ if (!response.ok) {
1581
+ throw new Error(`Failed to download frpc: ${response.status} ${response.statusText}`);
1582
+ }
1583
+ const buffer = Buffer.from(await response.arrayBuffer());
1584
+ if (platform() === "win32") {
1585
+ throw new Error("Windows ZIP extraction not implemented \u2014 please install frpc manually");
1586
+ }
1587
+ const tmpTar = join(BIN_DIR, `frpc-${FRP_VERSION}-${process.pid}-${Math.random().toString(36).slice(2, 8)}.tar.gz`);
1588
+ writeFileSync(tmpTar, buffer);
1589
+ try {
1590
+ const dirName = `frp_${FRP_VERSION}_${platform() === "darwin" ? "darwin" : "linux"}_${arch() === "x64" ? "amd64" : arch()}`;
1591
+ execSync(
1592
+ `tar -xzf "${tmpTar}" -C "${BIN_DIR}" --strip-components=1 "${dirName}/frpc"`,
1593
+ { stdio: "pipe" }
1594
+ );
1595
+ chmodSync(FRPC_BIN, 493);
1596
+ logger(`frpc installed at ${FRPC_BIN}`);
1597
+ } finally {
1598
+ try {
1599
+ unlinkSync(tmpTar);
1600
+ } catch {
1601
+ }
1602
+ }
1603
+ return FRPC_BIN;
1604
+ }
1605
+ function tomlStr(v) {
1606
+ return String(v).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
1607
+ }
1608
+ function generateFrpcConfig(config, proxies, admin) {
1609
+ const useWSS = config.serverPort === 443;
1610
+ const lines = [
1611
+ "# Auto-generated by svamp \u2014 do not edit",
1612
+ `serverAddr = "${tomlStr(config.serverAddr)}"`,
1613
+ `serverPort = ${config.serverPort}`,
1614
+ "",
1615
+ 'auth.method = "token"',
1616
+ `auth.token = "${tomlStr(config.authToken)}"`,
1617
+ "",
1618
+ "# Hypha JWT token for server-side authentication",
1619
+ ...config.hyphaToken ? [`metadatas.token = "${tomlStr(config.hyphaToken)}"`] : [],
1620
+ "",
1621
+ "# Transport",
1622
+ ...useWSS ? ['transport.protocol = "wss"'] : [],
1623
+ // 15s/45s heartbeat — 2x faster failure detection than the frpc default
1624
+ // (30s/90s). With matching server-side frps 0.68.x, dead-peer detection
1625
+ // settles in well under a minute, so the daemon-side recreate threshold
1626
+ // can be tightened too.
1627
+ "transport.heartbeatInterval = 15",
1628
+ "transport.heartbeatTimeout = 45",
1629
+ "transport.poolCount = 5",
1630
+ "",
1631
+ "# Don't exit on login failure \u2014 let frpc keep retrying",
1632
+ "loginFailExit = false",
1633
+ "",
1634
+ 'log.to = "console"',
1635
+ 'log.level = "info"',
1636
+ ""
1637
+ ];
1638
+ if (admin) {
1639
+ lines.push("# Local admin/status API (loopback only \u2014 polled by the svamp daemon)");
1640
+ lines.push('webServer.addr = "127.0.0.1"');
1641
+ lines.push(`webServer.port = ${admin.port}`);
1642
+ lines.push(`webServer.user = "${tomlStr(admin.user)}"`);
1643
+ lines.push(`webServer.password = "${tomlStr(admin.password)}"`);
1644
+ lines.push("");
1645
+ }
1646
+ for (const proxy of proxies) {
1647
+ lines.push(`[[proxies]]`);
1648
+ lines.push(`name = "${tomlStr(proxy.name)}"`);
1649
+ lines.push(`type = "${tomlStr(proxy.type)}"`);
1650
+ lines.push(`localIP = "${tomlStr(proxy.localIP || "127.0.0.1")}"`);
1651
+ lines.push(`localPort = ${proxy.localPort}`);
1652
+ if (proxy.subdomain) {
1653
+ lines.push(`subdomain = "${tomlStr(proxy.subdomain)}"`);
1654
+ }
1655
+ if (proxy.customDomains && proxy.customDomains.length > 0) {
1656
+ lines.push(`customDomains = [${proxy.customDomains.map((d) => `"${tomlStr(d)}"`).join(", ")}]`);
1657
+ }
1658
+ if (proxy.group) {
1659
+ lines.push(`loadBalancer.group = "${tomlStr(proxy.group)}"`);
1660
+ if (proxy.groupKey) {
1661
+ lines.push(`loadBalancer.groupKey = "${tomlStr(proxy.groupKey)}"`);
1662
+ }
1663
+ }
1664
+ if (proxy.healthCheckType) {
1665
+ lines.push(`healthCheck.type = "${tomlStr(proxy.healthCheckType)}"`);
1666
+ if (proxy.healthCheckType === "http" && proxy.healthCheckPath) {
1667
+ lines.push(`healthCheck.path = "${tomlStr(proxy.healthCheckPath)}"`);
1668
+ }
1669
+ lines.push(`healthCheck.intervalSeconds = ${proxy.healthCheckInterval || 10}`);
1670
+ lines.push(`healthCheck.timeoutSeconds = ${proxy.healthCheckTimeout || 3}`);
1671
+ lines.push(`healthCheck.maxFailed = ${proxy.healthCheckMaxFailed || 3}`);
1672
+ }
1673
+ if (proxy.type === "http") {
1674
+ lines.push("transport.useEncryption = false");
1675
+ lines.push("transport.useCompression = false");
1676
+ }
1677
+ lines.push("");
1678
+ }
1679
+ return lines.join("\n");
1680
+ }
1681
+ function getFreePort() {
1682
+ return new Promise((resolve, reject) => {
1683
+ const srv = createServer();
1684
+ srv.unref();
1685
+ srv.on("error", reject);
1686
+ srv.listen(0, "127.0.0.1", () => {
1687
+ const addr = srv.address();
1688
+ const port = typeof addr === "object" && addr ? addr.port : 0;
1689
+ srv.close(() => port ? resolve(port) : reject(new Error("no port")));
1690
+ });
1691
+ });
1692
+ }
1693
+ function parseFrpcStatus(payload, proxyNames) {
1694
+ const byName = /* @__PURE__ */ new Map();
1695
+ if (payload && typeof payload === "object") {
1696
+ for (const group of Object.values(payload)) {
1697
+ if (!Array.isArray(group)) continue;
1698
+ for (const p of group) {
1699
+ if (p && typeof p.name === "string") {
1700
+ byName.set(p.name, { status: String(p.status ?? ""), err: p.err ? String(p.err) : void 0 });
1701
+ }
1702
+ }
1703
+ }
1704
+ }
1705
+ const missing = [];
1706
+ const failed = [];
1707
+ for (const name of proxyNames) {
1708
+ const entry = byName.get(name);
1709
+ if (!entry) {
1710
+ missing.push(name);
1711
+ continue;
1712
+ }
1713
+ if (entry.status !== "running") failed.push({ name, status: entry.status, err: entry.err });
1714
+ }
1715
+ return { allRunning: missing.length === 0 && failed.length === 0, missing, failed };
1716
+ }
1717
+ class FrpcTunnel {
1718
+ process = null;
1719
+ _connected = false;
1720
+ _destroyed = false;
1721
+ // #0460: in-flight guard. `this.process` is only set AFTER the `await ensureFrpc()`
1722
+ // inside connect(), so two connect() calls interleaved across that await (e.g. the
1723
+ // auto-restart timer firing while an external caller also connects) both pass the
1724
+ // `if (this.process) return` null-check and spawn a duplicate frpc child — the first
1725
+ // is overwritten and never killed, leaving a ghost tunnel registration. This flag is
1726
+ // set synchronously before the await and makes connect() idempotent across it.
1727
+ _connecting = false;
1728
+ configPath;
1729
+ options;
1730
+ serverConfig;
1731
+ log;
1732
+ logBuffer = [];
1733
+ maxLogLines = 200;
1734
+ proxies = [];
1735
+ // Health tracking
1736
+ _consecutiveErrors = 0;
1737
+ _lastConnectedAt = 0;
1738
+ _lastErrorAt = 0;
1739
+ _firstErrorAt = 0;
1740
+ _restartAttempts = 0;
1741
+ // Persistent-conflict give-up (kth-k8s nightly review 2026-08-09). A generation that has NEVER
1742
+ // brought a proxy online and only ever gets `router config conflict` / `already exists` has lost
1743
+ // the vhost race to another live generation — it must STOP hammering frps ~2/min forever rather
1744
+ // than retry indefinitely. Reset on any real `start proxy success`; only fires when a proxy was
1745
+ // never started (so a live tunnel riding out an frps failover is never torn down).
1746
+ _everStartedProxy = false;
1747
+ _conflictCount = 0;
1748
+ _conflictSince = 0;
1749
+ _restartTimer = null;
1750
+ // #0583: pending reconnect timer (cancelled on destroy)
1751
+ // End-to-end probe state (set when options.probeUrl is provided).
1752
+ _probeTimer = null;
1753
+ _burstTimer = null;
1754
+ // #0622: probe burst timer (cancelled on stopProbeLoop/destroy)
1755
+ _connectTimeoutTimer = null;
1756
+ // #0622: connect() 30s fallback (unref'd + cleared)
1757
+ _lastProbeOkAt = 0;
1758
+ _lastProbeFailAt = 0;
1759
+ _probeOk = false;
1760
+ // frpc admin/status API state (set when options.adminStatus is true).
1761
+ _adminPort = 0;
1762
+ _adminUser = "svamp";
1763
+ _adminPassword = randomUUID();
1764
+ _statusTimer = null;
1765
+ constructor(options) {
1766
+ this.options = options;
1767
+ this.log = options.log || ((msg) => console.log(`[FRPC] ${msg}`));
1768
+ const hyphaToken = options.serverConfig?.hyphaToken || process.env.HYPHA_TOKEN || "";
1769
+ this.serverConfig = {
1770
+ serverAddr: options.serverConfig?.serverAddr || process.env.FRPS_SERVER_ADDR || (isInCluster() ? FRPS_CLUSTER_ADDR : getFrpsServerAddr()),
1771
+ serverPort: options.serverConfig?.serverPort || getFrpsServerPort() || DEFAULT_FRPS_PORT,
1772
+ authToken: options.serverConfig?.authToken || process.env.FRPS_AUTH_TOKEN || FRPS_PUBLIC_TOKEN,
1773
+ hyphaToken,
1774
+ subDomainHost: options.serverConfig?.subDomainHost || process.env.FRPS_SUBDOMAIN_HOST || getFrpsSubdomainHost()
1775
+ };
1776
+ if (!this.serverConfig.hyphaToken) {
1777
+ throw new Error(
1778
+ 'Hypha token required for frpc authentication. Run "svamp login" or set HYPHA_TOKEN.'
1779
+ );
1780
+ }
1781
+ const machineFingerprint = getMachineFingerprint();
1782
+ const machineHash = createHash("sha256").update(machineFingerprint).digest("hex").slice(0, 6);
1783
+ const multiPort = options.ports.length > 1;
1784
+ this.proxies = options.ports.map((port) => {
1785
+ const standaloneHash = createHash("sha256").update(multiPort ? `${options.name}|${port}|${machineFingerprint}` : `${options.name}|${machineFingerprint}`).digest("hex").slice(0, 8);
1786
+ const groupSubdomainKey = multiPort ? `${options.group}|${port}` : `${options.group}`;
1787
+ const subdomain = options.subdomains?.get(port) || (options.group ? `${options.group}-${createHash("sha256").update(groupSubdomainKey).digest("hex").slice(0, 8)}` : `${options.name}-${standaloneHash}`);
1788
+ const proxyName = `${options.name}-${port}-${machineHash}`;
1789
+ return {
1790
+ name: proxyName,
1791
+ type: "http",
1792
+ localIP: options.localHost || "127.0.0.1",
1793
+ localPort: port,
1794
+ subdomain,
1795
+ group: options.group,
1796
+ groupKey: options.groupKey,
1797
+ healthCheckType: options.healthCheckType,
1798
+ healthCheckPath: options.healthCheckPath,
1799
+ healthCheckInterval: options.healthCheckInterval
1800
+ };
1801
+ });
1802
+ const configDir = join(homedir(), ".svamp", "frpc");
1803
+ mkdirSync(configDir, { recursive: true, mode: 448 });
1804
+ try {
1805
+ chmodSync(configDir, 448);
1806
+ } catch {
1807
+ }
1808
+ this.configPath = join(configDir, `${options.name}.toml`);
1809
+ }
1810
+ /**
1811
+ * #0583: schedule a backoff reconnect after the child went down (spawn error OR exit).
1812
+ * Extracted from the exit handler so BOTH the spawn-'error' and 'exit' paths share it, and
1813
+ * so it is unit-testable without a real spawn. No-op once destroyed.
1814
+ */
1815
+ _reconnectAfterDown(reason) {
1816
+ if (this._destroyed) return;
1817
+ const STABILITY_WINDOW_MS = 3e4;
1818
+ if (this._lastConnectedAt > 0 && Date.now() - this._lastConnectedAt >= STABILITY_WINDOW_MS) {
1819
+ this._restartAttempts = 0;
1820
+ }
1821
+ this._restartAttempts++;
1822
+ const delay = Math.min(3e3 * Math.pow(2, this._restartAttempts - 1), 6e4);
1823
+ this.log(`frpc down (${reason}), restarting in ${Math.round(delay / 1e3)}s (attempt ${this._restartAttempts})...`);
1824
+ this._restartTimer = setTimeout(() => {
1825
+ this._restartTimer = null;
1826
+ if (!this._destroyed) {
1827
+ this.connect().catch((err) => {
1828
+ this.options.onError?.(err);
1829
+ if (!this._destroyed && !this._restartTimer && !this.process) {
1830
+ this._reconnectAfterDown(`reconnect setup failed: ${err?.message ?? err}`);
1831
+ }
1832
+ });
1833
+ }
1834
+ }, delay);
1835
+ }
1836
+ /** Connect to frps by starting the frpc process. */
1837
+ async connect() {
1838
+ if (this._destroyed) return;
1839
+ if (this.process) return;
1840
+ if (this._connecting) return;
1841
+ this._connecting = true;
1842
+ let frpcPath;
1843
+ let admin;
1844
+ try {
1845
+ frpcPath = await ensureFrpc(this.log);
1846
+ if (this.options.adminStatus) {
1847
+ if (!this._adminPort) {
1848
+ try {
1849
+ this._adminPort = await getFreePort();
1850
+ } catch (err) {
1851
+ this.log(`admin port alloc failed: ${err?.message ?? err}`);
1852
+ }
1853
+ }
1854
+ if (this._adminPort) admin = { port: this._adminPort, user: this._adminUser, password: this._adminPassword };
1855
+ }
1856
+ const configContent = generateFrpcConfig(this.serverConfig, this.proxies, admin);
1857
+ writeFileSync(this.configPath, configContent, { mode: 384 });
1858
+ this.log(`Config written to ${this.configPath}`);
1859
+ } catch (err) {
1860
+ this._connecting = false;
1861
+ throw err;
1862
+ }
1863
+ if (this._destroyed) {
1864
+ this._connecting = false;
1865
+ return;
1866
+ }
1867
+ return new Promise((resolve, reject) => {
1868
+ const child = spawn(frpcPath, ["-c", this.configPath], {
1869
+ stdio: ["ignore", "pipe", "pipe"],
1870
+ env: { ...process.env }
1871
+ });
1872
+ this.process = child;
1873
+ if (child.pid) _livingFrpcPids.add(child.pid);
1874
+ this._connecting = false;
1875
+ let resolved = false;
1876
+ const onOutput = (data) => {
1877
+ const text = data.toString().trim();
1878
+ if (!text) return;
1879
+ for (const line of text.split("\n")) {
1880
+ this.logBuffer.push(line);
1881
+ if (this.logBuffer.length > this.maxLogLines) {
1882
+ this.logBuffer.shift();
1883
+ }
1884
+ }
1885
+ if (text.includes("start proxy success")) {
1886
+ this._everStartedProxy = true;
1887
+ this._conflictCount = 0;
1888
+ this._conflictSince = 0;
1889
+ }
1890
+ if (text.includes("start proxy success") || text.includes("login to server success")) {
1891
+ if (!this._connected) {
1892
+ this._connected = true;
1893
+ this._lastConnectedAt = Date.now();
1894
+ this._consecutiveErrors = 0;
1895
+ this._firstErrorAt = 0;
1896
+ this.options.onConnect?.();
1897
+ const probeConfigured = !!(this.options.probeUrl || this.options.probePath);
1898
+ if (!resolved && !probeConfigured) {
1899
+ resolved = true;
1900
+ resolve();
1901
+ }
1902
+ }
1903
+ }
1904
+ const errorLine = text.split("\n").find((line) => line.includes("[E]"));
1905
+ if (errorLine) {
1906
+ this._consecutiveErrors++;
1907
+ this._lastErrorAt = Date.now();
1908
+ if (this._firstErrorAt === 0) this._firstErrorAt = Date.now();
1909
+ this.options.onError?.(new Error(errorLine));
1910
+ if (isFrpcSubdomainConflict(errorLine) && !this._everStartedProxy) {
1911
+ if (this._conflictSince === 0) this._conflictSince = Date.now();
1912
+ this._conflictCount++;
1913
+ const CONFLICT_GIVEUP_ATTEMPTS = Number(process.env.SVAMP_FRPC_CONFLICT_GIVEUP_ATTEMPTS || 6);
1914
+ const CONFLICT_GIVEUP_MS = Number(process.env.SVAMP_FRPC_CONFLICT_GIVEUP_MS || 6e4);
1915
+ const elapsed = Date.now() - this._conflictSince;
1916
+ if (shouldGiveUpOnConflict({
1917
+ everStartedProxy: this._everStartedProxy,
1918
+ conflictCount: this._conflictCount,
1919
+ elapsedMs: elapsed,
1920
+ minAttempts: CONFLICT_GIVEUP_ATTEMPTS,
1921
+ minElapsedMs: CONFLICT_GIVEUP_MS
1922
+ })) {
1923
+ this.log(
1924
+ `Giving up: ${this._conflictCount} subdomain conflicts over ${Math.round(elapsed / 1e3)}s with no proxy ever online \u2014 another generation owns this vhost. Stopping to avoid endless frps churn.`
1925
+ );
1926
+ this.options.onError?.(new Error(`frpc persistent subdomain conflict (${this._conflictCount}x) \u2014 giving up`));
1927
+ this.destroy();
1928
+ }
1929
+ }
1930
+ }
1931
+ };
1932
+ child.stdout?.on("data", onOutput);
1933
+ child.stderr?.on("data", onOutput);
1934
+ let downHandled = false;
1935
+ const onChildDown = (rejectErr) => {
1936
+ if (downHandled) return;
1937
+ downHandled = true;
1938
+ this._connected = false;
1939
+ if (child.pid) _livingFrpcPids.delete(child.pid);
1940
+ this.process = null;
1941
+ this.options.onDisconnect?.();
1942
+ if (!resolved) {
1943
+ resolved = true;
1944
+ reject(rejectErr);
1945
+ }
1946
+ this._reconnectAfterDown(rejectErr.message);
1947
+ };
1948
+ child.on("error", (err) => {
1949
+ this.options.onError?.(err);
1950
+ onChildDown(err instanceof Error ? err : new Error(String(err)));
1951
+ });
1952
+ child.on("exit", (code) => {
1953
+ onChildDown(new Error(`frpc exited with code ${code}`));
1954
+ });
1955
+ if (this.options.probeUrl || this.options.probePath) {
1956
+ this.startProbeLoop({
1957
+ onFirstOk: () => {
1958
+ if (!resolved) {
1959
+ resolved = true;
1960
+ resolve();
1961
+ }
1962
+ }
1963
+ });
1964
+ } else if (this.options.adminStatus && this._adminPort) {
1965
+ this.startAdminStatusLoop();
1966
+ }
1967
+ if (this._connectTimeoutTimer) {
1968
+ clearTimeout(this._connectTimeoutTimer);
1969
+ this._connectTimeoutTimer = null;
1970
+ }
1971
+ this._connectTimeoutTimer = setTimeout(() => {
1972
+ this._connectTimeoutTimer = null;
1973
+ if (!resolved) {
1974
+ resolved = true;
1975
+ this.log("frpc connection timeout \u2014 continuing (may still connect)");
1976
+ resolve();
1977
+ }
1978
+ }, 3e4);
1979
+ this._connectTimeoutTimer.unref?.();
1980
+ });
1981
+ }
1982
+ /** Resolve the effective probe URL, honoring probeUrl > probePath. */
1983
+ resolveProbeUrl() {
1984
+ if (this.options.probeUrl) return this.options.probeUrl;
1985
+ if (this.options.probePath) {
1986
+ const base = this.getUrls().get(this.options.ports[0]);
1987
+ if (!base) return null;
1988
+ const sep = this.options.probePath.startsWith("/") ? "" : "/";
1989
+ return `${base}${sep}${this.options.probePath}`;
1990
+ }
1991
+ return null;
1992
+ }
1993
+ /**
1994
+ * Start an end-to-end probe loop against the resolved probe URL.
1995
+ * Updates _probeOk + timestamps and fires onProbeFail on transitions.
1996
+ * Called from connect(); cleaned up in destroy().
1997
+ */
1998
+ startProbeLoop(opts = {}) {
1999
+ const probeUrl = this.resolveProbeUrl();
2000
+ if (!probeUrl) return;
2001
+ this.stopProbeLoop();
2002
+ const intervalMs = this.options.probeIntervalMs ?? 3e4;
2003
+ let firstOkFired = false;
2004
+ let inFlight = false;
2005
+ const runProbe = async () => {
2006
+ if (this._destroyed || inFlight) return;
2007
+ inFlight = true;
2008
+ try {
2009
+ const ctrl = new AbortController();
2010
+ const timer = setTimeout(() => ctrl.abort(), 1e4);
2011
+ let resp;
2012
+ try {
2013
+ resp = await fetch(probeUrl, {
2014
+ method: "GET",
2015
+ signal: ctrl.signal,
2016
+ // Don't follow redirects — we just want any 2xx/3xx.
2017
+ redirect: "manual",
2018
+ headers: { "User-Agent": "svamp-frpc-probe/1" }
2019
+ });
2020
+ } finally {
2021
+ clearTimeout(timer);
2022
+ }
2023
+ if (resp.status < 400) {
2024
+ const wasFailing = !this._probeOk;
2025
+ this._probeOk = true;
2026
+ this._lastProbeOkAt = Date.now();
2027
+ if (wasFailing) {
2028
+ this.log(`probe ok: ${probeUrl} (${resp.status})`);
2029
+ }
2030
+ if (!firstOkFired && opts.onFirstOk) {
2031
+ firstOkFired = true;
2032
+ opts.onFirstOk();
2033
+ }
2034
+ } else {
2035
+ throw new Error(`probe got ${resp.status}`);
2036
+ }
2037
+ } catch (err) {
2038
+ const wasOk = this._probeOk;
2039
+ this._probeOk = false;
2040
+ this._lastProbeFailAt = Date.now();
2041
+ if (wasOk) {
2042
+ this.log(`probe failed: ${err?.message || err}`);
2043
+ this.options.onProbeFail?.(err instanceof Error ? err : new Error(String(err)));
2044
+ }
2045
+ } finally {
2046
+ inFlight = false;
2047
+ }
2048
+ };
2049
+ const initialDelays = [500, 2e3, 5e3, 1e4];
2050
+ let burstIdx = 0;
2051
+ if (this._burstTimer) clearInterval(this._burstTimer);
2052
+ this._burstTimer = setInterval(() => {
2053
+ if (this._destroyed || firstOkFired || burstIdx >= initialDelays.length) {
2054
+ if (this._burstTimer) {
2055
+ clearInterval(this._burstTimer);
2056
+ this._burstTimer = null;
2057
+ }
2058
+ return;
2059
+ }
2060
+ burstIdx++;
2061
+ void runProbe();
2062
+ }, 1500);
2063
+ if (intervalMs > 0) {
2064
+ this._probeTimer = setInterval(runProbe, intervalMs);
2065
+ }
2066
+ void runProbe();
2067
+ }
2068
+ stopProbeLoop() {
2069
+ if (this._probeTimer) {
2070
+ clearInterval(this._probeTimer);
2071
+ this._probeTimer = null;
2072
+ }
2073
+ if (this._burstTimer) {
2074
+ clearInterval(this._burstTimer);
2075
+ this._burstTimer = null;
2076
+ }
2077
+ }
2078
+ /**
2079
+ * Poll frpc's loopback admin API (`/api/status`) to track whether this
2080
+ * tunnel's proxies are registered ("running"). Feeds the same `_probeOk` /
2081
+ * staleness fields the daemon health loop watches, so a ghosted/stuck proxy
2082
+ * is detected and recreated even with no HTTP health endpoint on the backend.
2083
+ *
2084
+ * Conservative semantics (critical infra — must not churn healthy tunnels):
2085
+ * - all proxies "running" → ok (refresh lastProbeOkAt)
2086
+ * - a proxy present but not running, or missing after a grace period
2087
+ * → fail (sets lastProbeFailAt; daemon recreates after staleness)
2088
+ * - admin API unreachable → INCONCLUSIVE: leave _probeOk untouched
2089
+ * (a transient blip can't flip ok→fail)
2090
+ */
2091
+ startAdminStatusLoop() {
2092
+ this.stopAdminStatusLoop();
2093
+ if (!this._adminPort) return;
2094
+ const proxyNames = this.proxies.map((p) => p.name);
2095
+ const intervalMs = this.options.probeIntervalMs ?? 3e4;
2096
+ const startedAt = Date.now();
2097
+ const auth = "Basic " + Buffer.from(`${this._adminUser}:${this._adminPassword}`).toString("base64");
2098
+ this._probeOk = true;
2099
+ this._lastProbeOkAt = Date.now();
2100
+ let inFlight = false;
2101
+ const poll = async () => {
2102
+ if (this._destroyed || inFlight || !this.process) return;
2103
+ inFlight = true;
2104
+ try {
2105
+ const ctrl = new AbortController();
2106
+ const timer = setTimeout(() => ctrl.abort(), 5e3);
2107
+ let payload;
2108
+ try {
2109
+ const resp = await fetch(`http://127.0.0.1:${this._adminPort}/api/status`, {
2110
+ headers: { Authorization: auth },
2111
+ signal: ctrl.signal
2112
+ });
2113
+ if (!resp.ok) throw new Error(`admin status ${resp.status}`);
2114
+ payload = await resp.json();
2115
+ } finally {
2116
+ clearTimeout(timer);
2117
+ }
2118
+ const { allRunning, missing, failed } = parseFrpcStatus(payload, proxyNames);
2119
+ const graceElapsed = Date.now() - startedAt > 2e4;
2120
+ if (allRunning || !failed.length && !graceElapsed) {
2121
+ const wasFailing = !this._probeOk;
2122
+ this._probeOk = true;
2123
+ this._lastProbeOkAt = Date.now();
2124
+ if (wasFailing) this.log(`admin status ok: all proxies running`);
2125
+ } else {
2126
+ const wasOk = this._probeOk;
2127
+ this._probeOk = false;
2128
+ this._lastProbeFailAt = Date.now();
2129
+ if (wasOk) {
2130
+ const detail = [
2131
+ ...failed.map((f) => `${f.name}=${f.status}${f.err ? ` (${f.err})` : ""}`),
2132
+ ...missing.map((m) => `${m}=missing`)
2133
+ ].join(", ");
2134
+ this.log(`admin status fail: ${detail}`);
2135
+ this.options.onProbeFail?.(new Error(`frpc proxy not running: ${detail}`));
2136
+ }
2137
+ }
2138
+ } catch {
2139
+ } finally {
2140
+ inFlight = false;
2141
+ }
2142
+ };
2143
+ if (intervalMs > 0) this._statusTimer = setInterval(poll, intervalMs);
2144
+ setTimeout(() => void poll(), 3e3);
2145
+ }
2146
+ stopAdminStatusLoop() {
2147
+ if (this._statusTimer) {
2148
+ clearInterval(this._statusTimer);
2149
+ this._statusTimer = null;
2150
+ }
2151
+ }
2152
+ /** Disconnect and stop the frpc process. */
2153
+ destroy() {
2154
+ this._destroyed = true;
2155
+ this._connected = false;
2156
+ this.stopProbeLoop();
2157
+ this.stopAdminStatusLoop();
2158
+ if (this._restartTimer) {
2159
+ clearTimeout(this._restartTimer);
2160
+ this._restartTimer = null;
2161
+ }
2162
+ if (this._connectTimeoutTimer) {
2163
+ clearTimeout(this._connectTimeoutTimer);
2164
+ this._connectTimeoutTimer = null;
2165
+ }
2166
+ if (this.process) {
2167
+ if (this.process.pid) _livingFrpcPids.delete(this.process.pid);
2168
+ this.process.kill("SIGTERM");
2169
+ const p = this.process;
2170
+ const killTimer = setTimeout(() => {
2171
+ try {
2172
+ if (p.exitCode === null && p.signalCode === null) p.kill("SIGKILL");
2173
+ } catch {
2174
+ }
2175
+ }, 5e3);
2176
+ killTimer.unref?.();
2177
+ p.once("exit", () => clearTimeout(killTimer));
2178
+ this.process = null;
2179
+ }
2180
+ try {
2181
+ unlinkSync(this.configPath);
2182
+ } catch {
2183
+ }
2184
+ }
2185
+ /** Whether the frpc tunnel is connected. */
2186
+ get isConnected() {
2187
+ return this._connected && this.process !== null;
2188
+ }
2189
+ /** Snapshot of tunnel health for monitoring. */
2190
+ get status() {
2191
+ return {
2192
+ connected: this.isConnected,
2193
+ pid: this.process?.pid ?? null,
2194
+ name: this.options.name,
2195
+ ports: this.options.ports,
2196
+ serverAddr: this.serverConfig.serverAddr,
2197
+ recentLogs: this.logBuffer.slice(-10),
2198
+ consecutiveErrors: this._consecutiveErrors,
2199
+ lastConnectedAt: this._lastConnectedAt,
2200
+ lastErrorAt: this._lastErrorAt,
2201
+ firstErrorAt: this._firstErrorAt,
2202
+ restartAttempts: this._restartAttempts,
2203
+ failingDurationMs: this._firstErrorAt > 0 ? Date.now() - this._firstErrorAt : 0,
2204
+ probe: this.resolveProbeUrl() || this.options.adminStatus && this._adminPort ? {
2205
+ url: this.resolveProbeUrl() || `frpc-admin:${this._adminPort}/api/status`,
2206
+ ok: this._probeOk,
2207
+ lastOkAt: this._lastProbeOkAt,
2208
+ lastFailAt: this._lastProbeFailAt,
2209
+ stalenessMs: this._lastProbeOkAt > 0 ? Date.now() - this._lastProbeOkAt : 0
2210
+ } : void 0
2211
+ };
2212
+ }
2213
+ /** Update the Hypha token. Rewrites config; takes effect on next frpc restart. */
2214
+ updateToken(newToken) {
2215
+ this.serverConfig.hyphaToken = newToken;
2216
+ const admin = this.options.adminStatus && this._adminPort ? { port: this._adminPort, user: this._adminUser, password: this._adminPassword } : void 0;
2217
+ const configContent = generateFrpcConfig(this.serverConfig, this.proxies, admin);
2218
+ writeFileSync(this.configPath, configContent, { mode: 384 });
2219
+ this.log("Config updated with fresh token");
2220
+ }
2221
+ /** Get the public URLs for each port. */
2222
+ getUrls() {
2223
+ const urls = /* @__PURE__ */ new Map();
2224
+ const base = this.serverConfig.subDomainHost || "svc.hypha.amun.ai";
2225
+ for (const proxy of this.proxies) {
2226
+ const subdomain = proxy.subdomain || proxy.name;
2227
+ urls.set(proxy.localPort, `https://${subdomain}.${base}`);
2228
+ }
2229
+ return urls;
2230
+ }
2231
+ }
2232
+ async function runFrpcTunnel(name, ports, serverConfig, tunnelOptions) {
2233
+ const portList = ports.join(", ");
2234
+ const tunnel = new FrpcTunnel({
2235
+ name,
2236
+ ports,
2237
+ serverConfig,
2238
+ ...tunnelOptions,
2239
+ onConnect: () => {
2240
+ console.log(`Tunnel connected: ${name} \u2192 localhost:[${portList}]`);
2241
+ const urls = tunnel.getUrls();
2242
+ for (const [port, url] of urls) {
2243
+ console.log(` ${port} \u2192 ${url}`);
2244
+ }
2245
+ },
2246
+ onDisconnect: () => {
2247
+ console.log("Tunnel disconnected, frpc will auto-reconnect...");
2248
+ },
2249
+ onError: (err) => {
2250
+ console.error(`Tunnel error: ${err.message}`);
2251
+ }
2252
+ });
2253
+ const cleanup = () => {
2254
+ console.log("\nTunnel shutting down...");
2255
+ tunnel.destroy();
2256
+ process.exit(0);
2257
+ };
2258
+ process.on("SIGINT", cleanup);
2259
+ process.on("SIGTERM", cleanup);
2260
+ try {
2261
+ await tunnel.connect();
2262
+ console.log(`Tunnel is active. Press Ctrl+C to stop.`);
2263
+ await new Promise(() => {
2264
+ });
2265
+ } catch (err) {
2266
+ console.error(`Failed to establish tunnel: ${err.message}`);
2267
+ process.exit(1);
2268
+ }
2269
+ }
2270
+
2271
+ var frpc = /*#__PURE__*/Object.freeze({
2272
+ __proto__: null,
2273
+ FrpcTunnel: FrpcTunnel,
2274
+ ensureFrpc: ensureFrpc,
2275
+ generateFrpcConfig: generateFrpcConfig,
2276
+ getFreePort: getFreePort,
2277
+ isFrpcSubdomainConflict: isFrpcSubdomainConflict,
2278
+ killOrphanedFrpc: killOrphanedFrpc,
2279
+ parseFrpcStatus: parseFrpcStatus,
2280
+ runFrpcTunnel: runFrpcTunnel,
2281
+ selectOrphanedFrpcPids: selectOrphanedFrpcPids,
2282
+ shouldGiveUpOnConflict: shouldGiveUpOnConflict
2283
+ });
2284
+
1263
2285
  const MIME = {
1264
2286
  ".html": "text/html; charset=utf-8",
1265
2287
  ".htm": "text/html; charset=utf-8",
@@ -1928,188 +2950,6 @@ function escapeHtml(s) {
1928
2950
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1929
2951
  }
1930
2952
 
1931
- function svampHome() {
1932
- return process.env.SVAMP_HOME || join(homedir(), ".svamp");
1933
- }
1934
- function cacheFile() {
1935
- return join(svampHome(), "instance-config.json");
1936
- }
1937
- const CONFIG_FILENAME = "svamp.json";
1938
- let _config = null;
1939
- let _loaded = false;
1940
- let _cacheChecked = false;
1941
- function stripSlash$1(u) {
1942
- return u.replace(/\/+$/, "");
1943
- }
1944
- function getInstanceUrl() {
1945
- const raw = (process.env.SVAMP_INSTANCE_URL || "").trim();
1946
- return raw ? stripSlash$1(raw) : void 0;
1947
- }
1948
- function configUrlsFor(anchor) {
1949
- const base = stripSlash$1(anchor);
1950
- return [`${base}/${CONFIG_FILENAME}`, `${base}/.well-known/${CONFIG_FILENAME}`];
1951
- }
1952
- async function fetchConfig(anchor) {
1953
- for (const url of configUrlsFor(anchor)) {
1954
- try {
1955
- const resp = await fetch(url, { signal: AbortSignal.timeout(8e3) });
1956
- if (!resp.ok) continue;
1957
- const data = await resp.json();
1958
- if (data && typeof data === "object" && !Array.isArray(data)) {
1959
- return data;
1960
- }
1961
- } catch {
1962
- }
1963
- }
1964
- return null;
1965
- }
1966
- function readCache() {
1967
- try {
1968
- if (existsSync$1(cacheFile())) {
1969
- const data = JSON.parse(readFileSync$1(cacheFile(), "utf-8"));
1970
- if (data && typeof data === "object" && !Array.isArray(data)) return data;
1971
- }
1972
- } catch {
1973
- }
1974
- return null;
1975
- }
1976
- function writeCache(cfg) {
1977
- try {
1978
- mkdirSync(svampHome(), { recursive: true });
1979
- writeFileSync(cacheFile(), JSON.stringify(cfg, null, 2) + "\n", "utf-8");
1980
- } catch {
1981
- }
1982
- }
1983
- function applyConfig(cfg) {
1984
- _config = cfg;
1985
- if (cfg.hyphaServerUrl && typeof cfg.hyphaServerUrl === "string") {
1986
- process.env.HYPHA_SERVER_URL = stripSlash$1(cfg.hyphaServerUrl.trim());
1987
- }
1988
- }
1989
- async function loadInstanceConfig(opts = {}) {
1990
- if (_loaded && !opts.force) return _config;
1991
- const anchor = opts.anchor ? stripSlash$1(opts.anchor) : getInstanceUrl();
1992
- if (anchor) {
1993
- const fetched = await fetchConfig(anchor);
1994
- if (fetched) {
1995
- applyConfig(fetched);
1996
- writeCache(fetched);
1997
- _loaded = true;
1998
- _cacheChecked = true;
1999
- return _config;
2000
- }
2001
- }
2002
- const cached = readCache();
2003
- if (cached) applyConfig(cached);
2004
- _loaded = true;
2005
- _cacheChecked = true;
2006
- return _config;
2007
- }
2008
- function getLoadedConfig() {
2009
- if (!_loaded && !_cacheChecked) {
2010
- const cached = readCache();
2011
- if (cached) applyConfig(cached);
2012
- _cacheChecked = true;
2013
- }
2014
- return _config;
2015
- }
2016
- function clearInstanceConfigCache() {
2017
- _config = null;
2018
- _loaded = false;
2019
- _cacheChecked = false;
2020
- try {
2021
- if (existsSync$1(cacheFile())) rmSync(cacheFile());
2022
- } catch {
2023
- }
2024
- }
2025
- function __setConfigForTest(cfg) {
2026
- _config = cfg;
2027
- _loaded = true;
2028
- _cacheChecked = true;
2029
- if (cfg) applyConfig(cfg);
2030
- }
2031
- function __resetForTest() {
2032
- _config = null;
2033
- _loaded = false;
2034
- _cacheChecked = false;
2035
- }
2036
-
2037
- var instanceConfig = /*#__PURE__*/Object.freeze({
2038
- __proto__: null,
2039
- CONFIG_FILENAME: CONFIG_FILENAME,
2040
- __resetForTest: __resetForTest,
2041
- __setConfigForTest: __setConfigForTest,
2042
- clearInstanceConfigCache: clearInstanceConfigCache,
2043
- configUrlsFor: configUrlsFor,
2044
- getInstanceUrl: getInstanceUrl,
2045
- getLoadedConfig: getLoadedConfig,
2046
- loadInstanceConfig: loadInstanceConfig
2047
- });
2048
-
2049
- function stripSlash(u) {
2050
- return u.replace(/\/+$/, "");
2051
- }
2052
- function getHyphaServerUrl() {
2053
- const fromCfg = getLoadedConfig()?.hyphaServerUrl;
2054
- if (typeof fromCfg === "string" && fromCfg.trim()) return stripSlash(fromCfg.trim());
2055
- const env = (process.env.HYPHA_SERVER_URL || "").trim();
2056
- return env ? stripSlash(env) : void 0;
2057
- }
2058
- function getFrpsSubdomainHost() {
2059
- const fromCfg = getLoadedConfig()?.frps?.subdomainHost;
2060
- if (typeof fromCfg === "string" && fromCfg.trim()) return fromCfg.trim();
2061
- const env = (process.env.FRPS_SUBDOMAIN_HOST || "").trim();
2062
- if (env) return env;
2063
- throw new Error(
2064
- 'frps subdomain host is not configured. Serve a svamp.json with "frps.subdomainHost" at your frontend URL (svamp login <frontend-url>), or set FRPS_SUBDOMAIN_HOST (legacy). It is deployment-specific and never guessed.'
2065
- );
2066
- }
2067
- function getFrpsServerAddr() {
2068
- const fromCfg = getLoadedConfig()?.frps?.serverAddr;
2069
- if (typeof fromCfg === "string" && fromCfg.trim()) return fromCfg.trim();
2070
- const env = (process.env.FRPS_SERVER_ADDR || "").trim();
2071
- if (env) return env;
2072
- return `frps-ctrl.${getFrpsSubdomainHost()}`;
2073
- }
2074
- function getFrpsServerPort() {
2075
- const fromCfg = getLoadedConfig()?.frps?.serverPort;
2076
- if (typeof fromCfg === "number" && Number.isFinite(fromCfg)) return fromCfg;
2077
- const env = parseInt(process.env.FRPS_SERVER_PORT || "", 10);
2078
- return Number.isFinite(env) ? env : void 0;
2079
- }
2080
- function getSvampWebBaseUrl() {
2081
- const fromCfg = getLoadedConfig()?.web?.baseUrl;
2082
- if (typeof fromCfg === "string" && fromCfg.trim()) return stripSlash(fromCfg.trim());
2083
- const env = (process.env.SVAMP_WEB_URL || "").trim();
2084
- if (env) return stripSlash(env);
2085
- const instance = getInstanceUrl();
2086
- if (instance) return instance;
2087
- throw new Error(
2088
- 'Svamp web base URL is not configured. Serve a svamp.json with "web.baseUrl", log in with your frontend URL (svamp login <frontend-url>), or set SVAMP_WEB_URL (legacy).'
2089
- );
2090
- }
2091
- function getSkillsServerUrl() {
2092
- const fromCfg = getLoadedConfig()?.skills?.server;
2093
- if (typeof fromCfg === "string" && fromCfg.trim()) return stripSlash(fromCfg.trim());
2094
- const env = (process.env.HYPHA_SKILLS_SERVER || "").trim();
2095
- if (env) return stripSlash(env);
2096
- const server = getHyphaServerUrl();
2097
- if (server) return server;
2098
- throw new Error(
2099
- "Cannot determine skills marketplace server. Serve a svamp.json or set HYPHA_SERVER_URL."
2100
- );
2101
- }
2102
- function getSkillsWorkspace() {
2103
- const fromCfg = getLoadedConfig()?.skills?.workspace;
2104
- if (typeof fromCfg === "string" && fromCfg.trim()) return fromCfg.trim();
2105
- return (process.env.HYPHA_SKILLS_WORKSPACE || "").trim() || "hypha-cloud";
2106
- }
2107
- function getSkillsCollection() {
2108
- const fromCfg = getLoadedConfig()?.skills?.collection;
2109
- if (typeof fromCfg === "string" && fromCfg.trim()) return fromCfg.trim();
2110
- return (process.env.HYPHA_SKILLS_COLLECTION || "").trim() || "marketplace";
2111
- }
2112
-
2113
2953
  const SESSION_FILES_MOUNT = "svamp-session-files";
2114
2954
  function generateLinkToken() {
2115
2955
  return crypto.randomBytes(11).toString("hex");
@@ -2819,6 +3659,7 @@ class ServeManager {
2819
3659
  if (this.proxyServer) return;
2820
3660
  killOrphanedCaddy((m) => this.log(m));
2821
3661
  reapOrphanedManagedProcesses(this.managedPidDir, (m) => this.log(m));
3662
+ killOrphanedFrpc((m) => this.log(m));
2822
3663
  this.port = await tryReservePort(this.persistedPort);
2823
3664
  if (this.persistedPort && this.port !== this.persistedPort) {
2824
3665
  this.log(`\u26A0 Previous serve port ${this.persistedPort} unavailable \u2014 using ${this.port}. Downstream configs referencing the old port will need updating.`);
@@ -3428,7 +4269,7 @@ Connection: close\r
3428
4269
  const mount = this.mounts.get(mountName);
3429
4270
  const subdomainOverride = mount?.access === "link" && mount.linkToken ? /* @__PURE__ */ new Map([[this.port, buildLinkSubdomain(subdomainSafe, mount.linkToken)]]) : void 0;
3430
4271
  try {
3431
- const { FrpcTunnel } = await import('./frpc-CNZJM3qC.mjs');
4272
+ const { FrpcTunnel } = await Promise.resolve().then(function () { return frpc; });
3432
4273
  let tunnel;
3433
4274
  tunnel = new FrpcTunnel({
3434
4275
  name: tunnelName,
@@ -3526,7 +4367,7 @@ function writeEnvLines(lines) {
3526
4367
  }
3527
4368
  writeFileSync$1(file, lines.join("\n") + "\n", { encoding: "utf-8", mode: 384 });
3528
4369
  try {
3529
- chmodSync(file, 384);
4370
+ chmodSync$1(file, 384);
3530
4371
  } catch {
3531
4372
  }
3532
4373
  }
@@ -3794,7 +4635,7 @@ function saveAccounts(accounts, svampHome) {
3794
4635
  const payload = { version: 1, accounts };
3795
4636
  writeFileSync$1(file, JSON.stringify(payload, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
3796
4637
  try {
3797
- chmodSync(file, 384);
4638
+ chmodSync$1(file, 384);
3798
4639
  } catch {
3799
4640
  }
3800
4641
  }
@@ -3849,7 +4690,7 @@ function upsertAccount(input, svampHome) {
3849
4690
  const now = Date.now();
3850
4691
  const existing = input.id ? accounts.find((a) => a.id === input.id) : void 0;
3851
4692
  const account = {
3852
- id: existing?.id || input.id || randomUUID(),
4693
+ id: existing?.id || input.id || randomUUID$1(),
3853
4694
  label: input.label.trim(),
3854
4695
  vendor,
3855
4696
  method,
@@ -4108,7 +4949,7 @@ const ANTHROPIC_REDIRECT_URI = "https://console.anthropic.com/oauth/code/callbac
4108
4949
  const ANTHROPIC_SCOPES = "user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
4109
4950
  function generatePkce() {
4110
4951
  const verifier = randomBytes(32).toString("base64url");
4111
- const challenge = createHash("sha256").update(verifier).digest("base64url");
4952
+ const challenge = createHash$1("sha256").update(verifier).digest("base64url");
4112
4953
  return { verifier, challenge };
4113
4954
  }
4114
4955
  function buildAnthropicAuthUrl(challenge, state) {
@@ -4217,14 +5058,14 @@ function startAnthropicFlow(label) {
4217
5058
  pruneFlows();
4218
5059
  const { verifier, challenge } = generatePkce();
4219
5060
  const state = randomBytes(32).toString("base64url");
4220
- const flowId = randomUUID();
5061
+ const flowId = randomUUID$1();
4221
5062
  const authorizeUrl = buildAnthropicAuthUrl(challenge, state);
4222
5063
  flows.set(flowId, { id: flowId, vendor: "anthropic", label, createdAt: Date.now(), status: "pending", state, verifier });
4223
5064
  return { flowId, authorizeUrl };
4224
5065
  }
4225
5066
  function startOpenAiDeviceFlow(label, log, svampHome) {
4226
5067
  pruneFlows();
4227
- const flowId = randomUUID();
5068
+ const flowId = randomUUID$1();
4228
5069
  const codexHome = join$1(oauthFlowDir(flowId, svampHome), "codex");
4229
5070
  mkdirSync$1(codexHome, { recursive: true });
4230
5071
  const flow = { id: flowId, vendor: "openai", label, createdAt: Date.now(), status: "pending", codexHome, outputBuffer: "" };
@@ -6587,7 +7428,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
6587
7428
  const tunnels = handlers.tunnels;
6588
7429
  if (!tunnels) throw new Error("Tunnel management not available");
6589
7430
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
6590
- const { FrpcTunnel } = await import('./frpc-CNZJM3qC.mjs');
7431
+ const { FrpcTunnel } = await Promise.resolve().then(function () { return frpc; });
6591
7432
  const tunnel = new FrpcTunnel({
6592
7433
  name: params.name,
6593
7434
  ports: params.ports,
@@ -6846,7 +7687,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
6846
7687
  const sessionRpc = handlers.getSessionRPCHandlers?.(sid);
6847
7688
  if (sessionRpc?.saveChannel) {
6848
7689
  try {
6849
- const sharedKey = createHash("sha256").update(sid + (process.env.INTERNAL_API_SECRET || "")).digest("hex").slice(0, 32);
7690
+ const sharedKey = createHash$1("sha256").update(sid + (process.env.INTERNAL_API_SECRET || "")).digest("hex").slice(0, 32);
6850
7691
  const meetingChannel = {
6851
7692
  id: `meeting-${sid}`,
6852
7693
  name: `Meeting WISE (${sid})`,
@@ -7080,7 +7921,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
7080
7921
  }
7081
7922
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
7082
7923
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
7083
- const { toolsForRole } = await import('./sideband-gQ7oQEsx.mjs');
7924
+ const { toolsForRole } = await import('./sideband-C1db5sMF.mjs');
7084
7925
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
7085
7926
  return fmt(r2);
7086
7927
  }
@@ -7179,13 +8020,13 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
7179
8020
  hyphaWorkspace: u?.scope?.current_workspace
7180
8021
  });
7181
8022
  if (r.error || !r.sender) return { error: r.error || "unauthorized" };
7182
- const callId = "call_" + randomUUID().slice(0, 10);
8023
+ const callId = "call_" + randomUUID$1().slice(0, 10);
7183
8024
  const senderKey = (r.sender.name || "anonymous").toLowerCase();
7184
8025
  if (!statelessLimiter.tryAcquire(senderKey)) {
7185
8026
  return { ok: false, call_id: callId, status: "busy", error: "channel is busy (too many concurrent requests) \u2014 retry shortly" };
7186
8027
  }
7187
8028
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
7188
- const { queryCore } = await import('./commands-wQYdc5EK.mjs');
8029
+ const { queryCore } = await import('./commands-BY1lmXaM.mjs');
7189
8030
  const timeout = c.reply?.timeout_sec || 120;
7190
8031
  let result;
7191
8032
  try {
@@ -7325,7 +8166,7 @@ ${d?.error || "not found"}`;
7325
8166
  try {
7326
8167
  const saved = writeUpload(dir, v.target, dec.data);
7327
8168
  try {
7328
- new ChannelStore(dir).recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId: "up_" + randomUUID().slice(0, 10), outcome: "uploaded" });
8169
+ new ChannelStore(dir).recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId: "up_" + randomUUID$1().slice(0, 10), outcome: "uploaded" });
7329
8170
  } catch {
7330
8171
  }
7331
8172
  return { ok: true, name: saved.name, path: saved.relPath, bytes: saved.bytes };
@@ -7434,7 +8275,7 @@ function writeInboundContext(sessionId, ctx) {
7434
8275
  try {
7435
8276
  const p = ctxPath(sessionId);
7436
8277
  mkdirSync$1(join$1(SVAMP_HOME$2, "inbound"), { recursive: true });
7437
- const tmp = `${p}.${process.pid}.${randomUUID()}.tmp`;
8278
+ const tmp = `${p}.${process.pid}.${randomUUID$1()}.tmp`;
7438
8279
  writeFileSync$1(tmp, JSON.stringify({ ...ctx, deliveredAt: Date.now() }));
7439
8280
  renameSync(tmp, p);
7440
8281
  } catch {
@@ -7519,7 +8360,7 @@ function writeAwaiting(sessionId, map) {
7519
8360
  try {
7520
8361
  mkdirSync$1(join$1(SVAMP_HOME$2, "awaiting"), { recursive: true });
7521
8362
  const p = awaitPath(sessionId);
7522
- const tmp = `${p}.${process.pid}.${randomUUID()}.tmp`;
8363
+ const tmp = `${p}.${process.pid}.${randomUUID$1()}.tmp`;
7523
8364
  writeFileSync$1(tmp, JSON.stringify(map));
7524
8365
  renameSync(tmp, p);
7525
8366
  } catch {
@@ -7781,7 +8622,7 @@ function ensureOutpostToken(projectDir, session) {
7781
8622
  mkdirSync$1(dirname(file), { recursive: true });
7782
8623
  writeFileSync$1(file, JSON.stringify(rec, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
7783
8624
  try {
7784
- chmodSync(file, 384);
8625
+ chmodSync$1(file, 384);
7785
8626
  } catch {
7786
8627
  }
7787
8628
  return rec;
@@ -7859,7 +8700,7 @@ function savePairings(pairings, svampHome) {
7859
8700
  const payload = { version: 1, pairings };
7860
8701
  writeFileSync$1(file, JSON.stringify(payload, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
7861
8702
  try {
7862
- chmodSync(file, 384);
8703
+ chmodSync$1(file, 384);
7863
8704
  } catch {
7864
8705
  }
7865
8706
  }
@@ -8571,11 +9412,11 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
8571
9412
  if (role === "agent") {
8572
9413
  const data = { ...content };
8573
9414
  if ((data.type === "assistant" || data.type === "user") && !data.uuid) {
8574
- data.uuid = randomUUID();
9415
+ data.uuid = randomUUID$1();
8575
9416
  }
8576
9417
  wrappedContent = { role: "agent", content: { type: "output", data } };
8577
9418
  } else if (role === "event") {
8578
- wrappedContent = { role: "agent", content: { type: "event", id: randomUUID(), data: content } };
9419
+ wrappedContent = { role: "agent", content: { type: "event", id: randomUUID$1(), data: content } };
8579
9420
  } else if (role === "session") {
8580
9421
  wrappedContent = { role: "session", content: { type: "session", data: content } };
8581
9422
  } else {
@@ -8583,7 +9424,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
8583
9424
  wrappedContent = { role: "user", content: { type: "text", text } };
8584
9425
  }
8585
9426
  const msg = {
8586
- id: randomUUID(),
9427
+ id: randomUUID$1(),
8587
9428
  seq: nextSeq++,
8588
9429
  content: wrappedContent,
8589
9430
  localId: null,
@@ -8621,7 +9462,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
8621
9462
  const msg = {
8622
9463
  // #0816: CSPRNG — this id routes `svamp session inbox reply`, so a predictable
8623
9464
  // one lets a third party target another agent's reply thread.
8624
- messageId: "op_" + randomUUID().slice(0, 10),
9465
+ messageId: "op_" + randomUUID$1().slice(0, 10),
8625
9466
  body: `A remote machine connected as an outpost: ${xmlEscape(who)} (${xmlEscape(plat)}). You can now run shell commands on it \u2014 its output returns synchronously, like a local command. Run one with: svamp outpost exec "uname -a"`,
8626
9467
  timestamp: Date.now(),
8627
9468
  read: false,
@@ -8866,10 +9707,10 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
8866
9707
  const wrappedContent = parsed && parsed.role === "user" ? { role: "user", content: parsed.content } : { role: "user", content: { type: "text", text: typeof parsed === "string" ? parsed : JSON.stringify(parsed) } };
8867
9708
  if (isBtwEcho) wrappedContent.meta = { btw: true };
8868
9709
  const msg = {
8869
- id: randomUUID(),
9710
+ id: randomUUID$1(),
8870
9711
  seq: nextSeq++,
8871
9712
  content: wrappedContent,
8872
- localId: effectiveLocalId || randomUUID(),
9713
+ localId: effectiveLocalId || randomUUID$1(),
8873
9714
  createdAt: Date.now(),
8874
9715
  updatedAt: Date.now()
8875
9716
  };
@@ -9259,7 +10100,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
9259
10100
  hyphaWorkspace: u?.scope?.current_workspace
9260
10101
  });
9261
10102
  if (r.error || !r.sender) return { error: r.error || "unauthorized" };
9262
- const callId = "call_" + randomUUID().slice(0, 10);
10103
+ const callId = "call_" + randomUUID$1().slice(0, 10);
9263
10104
  const ownerEmail = metadata.sharing?.owner;
9264
10105
  const ownerCtx = ownerEmail ? { user: { email: ownerEmail, id: ownerEmail } } : void 0;
9265
10106
  try {
@@ -9344,7 +10185,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
9344
10185
  if (dec.error || !dec.data) return { error: dec.error || "invalid content" };
9345
10186
  const v = validateUpload({ projectDir: metadata.path, channel: c, filename: params.filename, bytes: dec.data.length, contentType: params.content_type });
9346
10187
  if (v.error || !v.target) return { error: v.error || "upload rejected" };
9347
- const callId = "call_" + randomUUID().slice(0, 10);
10188
+ const callId = "call_" + randomUUID$1().slice(0, 10);
9348
10189
  try {
9349
10190
  const saved = writeUpload(metadata.path, v.target, dec.data);
9350
10191
  channelStore.recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId, outcome: "uploaded" });
@@ -11067,10 +11908,10 @@ function computeCollectionConfigUpdate(existingConfig, extra) {
11067
11908
  return { changed, merged: { ...cfg, ...extra } };
11068
11909
  }
11069
11910
  function emailHash(email) {
11070
- return createHash("sha256").update(email.toLowerCase()).digest("hex").slice(0, 12);
11911
+ return createHash$1("sha256").update(email.toLowerCase()).digest("hex").slice(0, 12);
11071
11912
  }
11072
11913
  function sessionIdHash(sessionId) {
11073
- return createHash("sha256").update(sessionId).digest("hex").slice(0, 12);
11914
+ return createHash$1("sha256").update(sessionId).digest("hex").slice(0, 12);
11074
11915
  }
11075
11916
  function roleLabel(role) {
11076
11917
  switch (role) {
@@ -11190,7 +12031,7 @@ class SharingNotificationSync {
11190
12031
  const shareType = params.shareType || "session";
11191
12032
  const typeLabel = shareType === "machine" ? "Machine" : "Session";
11192
12033
  await this.publishUserEvent({
11193
- id: randomUUID(),
12034
+ id: randomUUID$1(),
11194
12035
  recipientEmail: params.recipientEmail.toLowerCase(),
11195
12036
  type: "sharing-invite",
11196
12037
  source: {
@@ -11240,7 +12081,7 @@ class SharingNotificationSync {
11240
12081
  } catch {
11241
12082
  }
11242
12083
  await this.publishUserEvent({
11243
- id: randomUUID(),
12084
+ id: randomUUID$1(),
11244
12085
  recipientEmail: recipientEmail.toLowerCase(),
11245
12086
  type: "sharing-revoked",
11246
12087
  source: {
@@ -11898,7 +12739,7 @@ class AcpBackend {
11898
12739
  }
11899
12740
  async startSession(initialPrompt) {
11900
12741
  if (this.disposed) throw new Error("Backend has been disposed");
11901
- const sessionId = randomUUID();
12742
+ const sessionId = randomUUID$1();
11902
12743
  this.emit({ type: "status", status: "starting" });
11903
12744
  let startupStatusErrorEmitted = false;
11904
12745
  try {
@@ -12048,7 +12889,7 @@ class AcpBackend {
12048
12889
  const extendedParams = params;
12049
12890
  const toolCall = extendedParams.toolCall;
12050
12891
  let toolName = toolCall?.kind || toolCall?.toolName || extendedParams.kind || "Unknown tool";
12051
- const toolCallId = toolCall?.id || randomUUID();
12892
+ const toolCallId = toolCall?.id || randomUUID$1();
12052
12893
  const permissionId = toolCallId;
12053
12894
  let input = {};
12054
12895
  if (toolCall) {
@@ -13289,7 +14130,7 @@ class CodexAppServerBackend {
13289
14130
  }
13290
14131
  listeners = [];
13291
14132
  client;
13292
- sessionId = randomUUID();
14133
+ sessionId = randomUUID$1();
13293
14134
  log;
13294
14135
  model;
13295
14136
  // #0438: codex's own approval policy + sandbox, derived from the svamp permission mode. Mutable
@@ -13434,7 +14275,7 @@ class CodexAppServerBackend {
13434
14275
  }
13435
14276
  }
13436
14277
  handleApproval(req) {
13437
- const id = req.callId || randomUUID();
14278
+ const id = req.callId || randomUUID$1();
13438
14279
  const reason = req.type === "exec" ? `Execute: ${(req.command ?? []).join(" ")}` : req.type === "patch" ? "Apply file changes" : req.toolName ? `Tool: ${req.toolName}` : "Codex requires approval";
13439
14280
  const payload = req.type === "exec" ? { toolName: "CodexBash", input: { command: req.command, cwd: req.cwd } } : req.type === "patch" ? { toolName: "CodexPatch", input: { fileChanges: req.fileChanges } } : { toolName: req.toolName ?? "CodexTool", input: req.input };
13440
14281
  this.emit({ type: "permission-request", id, reason, payload });
@@ -13480,7 +14321,7 @@ class CodexAppServerBackend {
13480
14321
  break;
13481
14322
  }
13482
14323
  case "exec_command_begin": {
13483
- const callId = String(event.call_id ?? event.callId ?? randomUUID());
14324
+ const callId = String(event.call_id ?? event.callId ?? randomUUID$1());
13484
14325
  const command = Array.isArray(event.command) ? event.command.join(" ") : String(event.command ?? "");
13485
14326
  this.emit({ type: "tool-call", toolName: "CodexBash", callId, args: { command, cwd: event.cwd } });
13486
14327
  break;
@@ -13491,7 +14332,7 @@ class CodexAppServerBackend {
13491
14332
  break;
13492
14333
  }
13493
14334
  case "patch_apply_begin": {
13494
- const callId = String(event.call_id ?? event.callId ?? randomUUID());
14335
+ const callId = String(event.call_id ?? event.callId ?? randomUUID$1());
13495
14336
  const { filePath, patch, kind } = summarizeCodexChanges(event.changes);
13496
14337
  this.emit({ type: "tool-call", toolName: "CodexPatch", callId, args: {
13497
14338
  filePath: filePath ?? event.file_path ?? event.filePath ?? null,
@@ -13511,7 +14352,7 @@ class CodexAppServerBackend {
13511
14352
  break;
13512
14353
  }
13513
14354
  case "mcp_tool_call_begin": {
13514
- const callId = String(event.call_id ?? event.callId ?? randomUUID());
14355
+ const callId = String(event.call_id ?? event.callId ?? randomUUID$1());
13515
14356
  this.emit({ type: "tool-call", toolName: String(event.tool ?? "CodexMcpTool"), callId, args: { server: event.server, arguments: event.arguments } });
13516
14357
  break;
13517
14358
  }
@@ -14459,7 +15300,7 @@ async function stageCredentialsForSharing(sessionId) {
14459
15300
  } catch {
14460
15301
  }
14461
15302
  }
14462
- if (!credentialsCopied && platform() === "darwin") {
15303
+ if (!credentialsCopied && platform$1() === "darwin") {
14463
15304
  const stagedCredFile = join$1(stagedClaudeDir, ".credentials.json");
14464
15305
  const hasExistingCredentials = existsSync(stagedCredFile);
14465
15306
  if (!hasExistingCredentials) {
@@ -14851,7 +15692,7 @@ class ProcessSupervisor {
14851
15692
  }
14852
15693
  const fullSpec = {
14853
15694
  ...spec,
14854
- id: randomUUID$1(),
15695
+ id: randomUUID(),
14855
15696
  createdAt: Date.now()
14856
15697
  };
14857
15698
  const entry = this.makeEntry(fullSpec);
@@ -15614,7 +16455,7 @@ function _updateIssueUnlocked(projectRoot, id, patch) {
15614
16455
  if (wasArchived !== nowArchived) {
15615
16456
  const oldPath = issuePath(projectRoot, cur.id, wasArchived);
15616
16457
  try {
15617
- if (existsSync(oldPath)) unlinkSync(oldPath);
16458
+ if (existsSync(oldPath)) unlinkSync$1(oldPath);
15618
16459
  } catch {
15619
16460
  }
15620
16461
  }
@@ -15890,7 +16731,7 @@ function removeWorkflow(projectRoot, name) {
15890
16731
  const p = workflowPath(projectRoot, name);
15891
16732
  if (!existsSync(p)) return false;
15892
16733
  try {
15893
- unlinkSync(p);
16734
+ unlinkSync$1(p);
15894
16735
  return true;
15895
16736
  } catch {
15896
16737
  return false;
@@ -16906,7 +17747,7 @@ process.stdin.resume();
16906
17747
  const cleanup = () => {
16907
17748
  for (const p of cleanupPaths) {
16908
17749
  try {
16909
- if (existsSync(p)) unlinkSync(p);
17750
+ if (existsSync(p)) unlinkSync$1(p);
16910
17751
  } catch {
16911
17752
  }
16912
17753
  }
@@ -17552,7 +18393,7 @@ function readPendingRestart() {
17552
18393
  }
17553
18394
  function clearPendingRestart() {
17554
18395
  try {
17555
- if (existsSync$1(PENDING_RESTART_FILE)) unlinkSync$1(PENDING_RESTART_FILE);
18396
+ if (existsSync$1(PENDING_RESTART_FILE)) unlinkSync(PENDING_RESTART_FILE);
17556
18397
  } catch {
17557
18398
  }
17558
18399
  }
@@ -17565,7 +18406,7 @@ function writeStopMarker(reason) {
17565
18406
  }
17566
18407
  function clearStopMarker() {
17567
18408
  try {
17568
- if (existsSync$1(DAEMON_STOP_MARKER_FILE)) unlinkSync$1(DAEMON_STOP_MARKER_FILE);
18409
+ if (existsSync$1(DAEMON_STOP_MARKER_FILE)) unlinkSync(DAEMON_STOP_MARKER_FILE);
17569
18410
  } catch {
17570
18411
  }
17571
18412
  }
@@ -17962,7 +18803,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
17962
18803
  url: String(l.url).trim(),
17963
18804
  ...l.icon ? { icon: String(l.icon) } : {},
17964
18805
  // #0266: carry the service backing ({kind,name}) so the Launch Pad can decommission it.
17965
- ...l.service && typeof l.service.name === "string" && (l.service.kind === "tunnel" || l.service.kind === "process") ? { service: { kind: l.service.kind, name: String(l.service.name) } } : {}
18806
+ ...l.service && typeof l.service.name === "string" && (l.service.kind === "tunnel" || l.service.kind === "process" || l.service.kind === "serve") ? { service: { kind: l.service.kind, name: String(l.service.name) } } : {}
17966
18807
  }));
17967
18808
  const cur = meta.sessionLinks;
17968
18809
  if (JSON.stringify(cur || []) !== JSON.stringify(links)) {
@@ -18099,7 +18940,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
18099
18940
  }
18100
18941
  if (wasStopped) {
18101
18942
  const eq = getMetadata().messageQueue || [];
18102
- setMetadata((m) => ({ ...m, messageQueue: [...eq, { id: randomUUID$1(), text: loopResumeMessage(directory, sessionId), displayText: "\u{1F501} Resuming loop (extended)", createdAt: Date.now() }] }));
18943
+ setMetadata((m) => ({ ...m, messageQueue: [...eq, { id: randomUUID(), text: loopResumeMessage(directory, sessionId), displayText: "\u{1F501} Resuming loop (extended)", createdAt: Date.now() }] }));
18103
18944
  onLoopActivated?.();
18104
18945
  }
18105
18946
  sessionService.pushMessage({ type: "message", message: `\u{1F501} Loop extended (+${grant.extension.grantedTurns} turns, ${grant.used}/${grant.max} used)${wasStopped ? " \u2014 resuming" : ""}. Reason: ${grant.extension.reason}`, level: "warning" }, "event");
@@ -18155,7 +18996,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
18155
18996
  } catch {
18156
18997
  }
18157
18998
  const eq = getMetadata().messageQueue || [];
18158
- setMetadata((m) => ({ ...m, messageQueue: [...eq, { id: randomUUID$1(), text: loopResumeMessage(directory, sessionId), displayText: "\u{1F501} Resuming loop", createdAt: Date.now() }] }));
18999
+ setMetadata((m) => ({ ...m, messageQueue: [...eq, { id: randomUUID(), text: loopResumeMessage(directory, sessionId), displayText: "\u{1F501} Resuming loop", createdAt: Date.now() }] }));
18159
19000
  onLoopActivated?.();
18160
19001
  }
18161
19002
  if (!wasStopped && (newMax != null || bumpedBudget)) {
@@ -18188,14 +19029,14 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
18188
19029
  }
18189
19030
  writeGoalLoopState(directory, sessionId, { active: true, phase: "running", durable, iteration: 0, task: condition, goal_task: task, until, oracle, max_iterations: hardMax, budget, started_at: Date.now(), holds: 0, ledger: { turns: 0, tokens: 0, window: [] }, progress_history: [], auto_resumes: 0, verify_errors: 0 });
18190
19031
  const existingQueue = getMetadata().messageQueue || [];
18191
- setMetadata((m) => ({ ...m, messageQueue: [...existingQueue, { id: randomUUID$1(), text: `/goal ${condition}`, displayText: `\u{1F3AF} Goal: ${(task || until || "").slice(0, 100)}`, createdAt: Date.now() }] }));
19032
+ setMetadata((m) => ({ ...m, messageQueue: [...existingQueue, { id: randomUUID(), text: `/goal ${condition}`, displayText: `\u{1F3AF} Goal: ${(task || until || "").slice(0, 100)}`, createdAt: Date.now() }] }));
18192
19033
  onLoopActivated?.();
18193
19034
  sessionService.pushMessage({ type: "message", message: `\u{1F3AF} Goal set: ${condition.slice(0, 180)}${condition.length > 180 ? "\u2026" : ""}` }, "event");
18194
19035
  logger.log(`[svampConfig] /goal set: "${condition.slice(0, 80)}"`);
18195
19036
  } else {
18196
19037
  writeGoalLoopState(directory, sessionId, { active: false, phase: "cancelled", completed_at: Date.now() });
18197
19038
  const existingQueue = getMetadata().messageQueue || [];
18198
- setMetadata((m) => ({ ...m, messageQueue: [...existingQueue, { id: randomUUID$1(), text: "/goal clear", displayText: "\u{1F3AF} Goal cleared", createdAt: Date.now() }] }));
19039
+ setMetadata((m) => ({ ...m, messageQueue: [...existingQueue, { id: randomUUID(), text: "/goal clear", displayText: "\u{1F3AF} Goal cleared", createdAt: Date.now() }] }));
18199
19040
  onLoopActivated?.();
18200
19041
  sessionService.pushMessage({ type: "message", message: "Goal cleared." }, "event");
18201
19042
  logger.log(`[svampConfig] /goal cleared`);
@@ -18268,17 +19109,17 @@ function deletePersistedSession(sessionId) {
18268
19109
  if (entry) {
18269
19110
  const sessionFile = getSessionFilePath(entry.directory, sessionId);
18270
19111
  try {
18271
- if (existsSync$1(sessionFile)) unlinkSync$1(sessionFile);
19112
+ if (existsSync$1(sessionFile)) unlinkSync(sessionFile);
18272
19113
  } catch {
18273
19114
  }
18274
19115
  const messagesFile = getSessionMessagesPath(entry.directory, sessionId);
18275
19116
  try {
18276
- if (existsSync$1(messagesFile)) unlinkSync$1(messagesFile);
19117
+ if (existsSync$1(messagesFile)) unlinkSync(messagesFile);
18277
19118
  } catch {
18278
19119
  }
18279
19120
  const configFile = getSvampConfigPath(entry.directory, sessionId);
18280
19121
  try {
18281
- if (existsSync$1(configFile)) unlinkSync$1(configFile);
19122
+ if (existsSync$1(configFile)) unlinkSync(configFile);
18282
19123
  } catch {
18283
19124
  }
18284
19125
  const sessionDir = getSessionDir(entry.directory, sessionId);
@@ -18372,7 +19213,7 @@ function pruneDaemonLogs() {
18372
19213
  const plan = planLogPrune(entries, policy);
18373
19214
  for (const name of plan.deleteFiles) {
18374
19215
  try {
18375
- unlinkSync$1(join(LOGS_DIR, name));
19216
+ unlinkSync(join(LOGS_DIR, name));
18376
19217
  } catch {
18377
19218
  }
18378
19219
  }
@@ -18621,7 +19462,7 @@ async function startDaemon(options) {
18621
19462
  machineId = readFileSync$1(machineIdFile, "utf-8").trim();
18622
19463
  }
18623
19464
  if (!machineId) {
18624
- machineId = `machine-${os$1.hostname()}-${randomUUID$1().slice(0, 8)}`;
19465
+ machineId = `machine-${os$1.hostname()}-${randomUUID().slice(0, 8)}`;
18625
19466
  try {
18626
19467
  writeFileSync(machineIdFile, machineId, "utf-8");
18627
19468
  } catch {
@@ -18669,7 +19510,7 @@ async function startDaemon(options) {
18669
19510
  try {
18670
19511
  const dir = loadSessionIndex()[sessionId]?.directory;
18671
19512
  if (!dir) return;
18672
- const { reconcileServiceLinks } = await import('./agentCommands-DdOSpUEW.mjs');
19513
+ const { reconcileServiceLinks } = await import('./agentCommands-BJeGwkeX.mjs');
18673
19514
  const configPath = getSvampConfigPath(dir, sessionId);
18674
19515
  const config = readSvampConfig(configPath);
18675
19516
  const entries = Array.from(urls.entries());
@@ -18686,8 +19527,29 @@ async function startDaemon(options) {
18686
19527
  logger.log(`[exposed-tunnels] Link reconcile failed for ${tunnelName}: ${err?.message || err}`);
18687
19528
  }
18688
19529
  }
19530
+ async function reconcileMountSessionLink(sessionId, mountName, url) {
19531
+ if (!sessionId || !url) return;
19532
+ try {
19533
+ const dir = loadSessionIndex()[sessionId]?.directory;
19534
+ if (!dir) return;
19535
+ const { reconcileServiceLinks } = await import('./agentCommands-BJeGwkeX.mjs');
19536
+ const configPath = getSvampConfigPath(dir, sessionId);
19537
+ const config = readSvampConfig(configPath);
19538
+ const incoming = [{
19539
+ url: String(url),
19540
+ label: mountName,
19541
+ service: { kind: "serve", name: mountName }
19542
+ }];
19543
+ if (reconcileServiceLinks(config, incoming)) {
19544
+ writeSvampConfig(configPath, config);
19545
+ logger.log(`[serve] Reconciled Launch Pad link for mount ${mountName} \u2192 session ${sessionId}`);
19546
+ }
19547
+ } catch (err) {
19548
+ logger.log(`[serve] Link reconcile failed for mount ${mountName}: ${err?.message || err}`);
19549
+ }
19550
+ }
18689
19551
  async function createExposedTunnel(spec) {
18690
- const { FrpcTunnel } = await import('./frpc-CNZJM3qC.mjs');
19552
+ const { FrpcTunnel } = await Promise.resolve().then(function () { return frpc; });
18691
19553
  const tunnel = new FrpcTunnel({
18692
19554
  name: spec.name,
18693
19555
  ports: spec.ports,
@@ -18715,7 +19577,7 @@ async function startDaemon(options) {
18715
19577
  ensureAutoInstalledCommands(logger);
18716
19578
  (async () => {
18717
19579
  try {
18718
- const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-CnCpensV.mjs');
19580
+ const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-dbXgsGNK.mjs');
18719
19581
  beginClaudeVersionReconcile((msg) => logger.log(msg));
18720
19582
  } catch (e) {
18721
19583
  logger.log(`[claude-version] check failed: ${e?.message || e}`);
@@ -18764,7 +19626,7 @@ async function startDaemon(options) {
18764
19626
  logger.log(`[supervision] parent ${parentId.slice(0, 8)} not live \u2014 recording verdict on the backlog issue only (no in-memory wake)`);
18765
19627
  }
18766
19628
  if (send) Promise.resolve(send({
18767
- messageId: randomUUID$1(),
19629
+ messageId: randomUUID(),
18768
19630
  from: `agent:${v.sessionId}`,
18769
19631
  fromSession: v.sessionId,
18770
19632
  to: parentId,
@@ -18802,7 +19664,7 @@ ${v.guidance ? v.guidance + "\n" : ""}Criteria: ${v.criteria || "(none)"}
18802
19664
  return;
18803
19665
  }
18804
19666
  Promise.resolve(send({
18805
- messageId: randomUUID$1(),
19667
+ messageId: randomUUID(),
18806
19668
  from: "system:issues",
18807
19669
  subject: "issue follow-up",
18808
19670
  body: text,
@@ -19370,7 +20232,7 @@ ${parts.join("\n")}`);
19370
20232
  sessionMetadata = {
19371
20233
  ...sessionMetadata,
19372
20234
  messageQueue: [...existingQueue, {
19373
- id: randomUUID$1(),
20235
+ id: randomUUID(),
19374
20236
  text,
19375
20237
  displayText: "\u{1F501} Resuming loop",
19376
20238
  createdAt: Date.now()
@@ -19382,7 +20244,7 @@ ${parts.join("\n")}`);
19382
20244
  const existingQueue = sessionMetadata.messageQueue || [];
19383
20245
  sessionMetadata = {
19384
20246
  ...sessionMetadata,
19385
- messageQueue: [...existingQueue, { id: randomUUID$1(), text, displayText, createdAt: Date.now() }]
20247
+ messageQueue: [...existingQueue, { id: randomUUID(), text, displayText, createdAt: Date.now() }]
19386
20248
  };
19387
20249
  sessionService.updateMetadata(sessionMetadata);
19388
20250
  };
@@ -20498,7 +21360,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
20498
21360
  // NOT re-push a display bubble (that second copy has localId:null and the
20499
21361
  // frontend can't reconcile it → the message appears TWICE). Only re-send to
20500
21362
  // the model. Inbox/loop items (no flag) still get their displayText push.
20501
- messageQueue: [...existingQueue, { id: randomUUID$1(), text, createdAt: Date.now(), alreadyStored: true }]
21363
+ messageQueue: [...existingQueue, { id: randomUUID(), text, createdAt: Date.now(), alreadyStored: true }]
20502
21364
  };
20503
21365
  sessionService.updateMetadata(sessionMetadata);
20504
21366
  signalProcessing(false);
@@ -20655,7 +21517,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
20655
21517
  const existingQueue = sessionMetadata.messageQueue || [];
20656
21518
  sessionMetadata = {
20657
21519
  ...sessionMetadata,
20658
- messageQueue: [...existingQueue, { id: randomUUID$1(), text, createdAt: Date.now(), alreadyStored: true }]
21520
+ messageQueue: [...existingQueue, { id: randomUUID(), text, createdAt: Date.now(), alreadyStored: true }]
20659
21521
  };
20660
21522
  sessionService.updateMetadata(sessionMetadata);
20661
21523
  signalProcessing(true);
@@ -21032,11 +21894,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
21032
21894
  });
21033
21895
  },
21034
21896
  onIssue: async (params) => {
21035
- const { issueRpc } = await import('./rpc-Cx6KsBYC.mjs');
21897
+ const { issueRpc } = await import('./rpc-TXF-di9u.mjs');
21036
21898
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
21037
21899
  },
21038
21900
  onWorkflow: async (params) => {
21039
- const { workflowRpc } = await import('./rpc-CclCeDcu.mjs');
21901
+ const { workflowRpc } = await import('./rpc-FGp88Yxp.mjs');
21040
21902
  return workflowRpc(params?.cwd || directory, params || {});
21041
21903
  },
21042
21904
  onRipgrep: async (args, cwd) => {
@@ -21269,7 +22131,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
21269
22131
  enqueueLoopContinue();
21270
22132
  processMessageQueueRef?.();
21271
22133
  };
21272
- pidToTrackedSession.set(randomUUID$1(), trackedSession);
22134
+ pidToTrackedSession.set(randomUUID(), trackedSession);
21273
22135
  sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
21274
22136
  sessionService.updateMetadata(sessionMetadata);
21275
22137
  logger.log(`Session ${sessionId} registered on Hypha, waiting for first message to spawn Claude`);
@@ -21474,7 +22336,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
21474
22336
  ...sessionMetadata,
21475
22337
  // #0411: alreadyStored — see Claude path. deliverUserMessageCore already stored
21476
22338
  // this user message; the drain must not re-push a localId:null duplicate.
21477
- messageQueue: [...existingQueue, { id: randomUUID$1(), text, createdAt: Date.now(), alreadyStored: true }]
22339
+ messageQueue: [...existingQueue, { id: randomUUID(), text, createdAt: Date.now(), alreadyStored: true }]
21478
22340
  };
21479
22341
  sessionService.updateMetadata(sessionMetadata);
21480
22342
  return;
@@ -21486,7 +22348,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
21486
22348
  ...sessionMetadata,
21487
22349
  // #0411: alreadyStored — deliverUserMessageCore already stored this
21488
22350
  // user message; the drain must not re-push a localId:null duplicate.
21489
- messageQueue: [...existingQueue, { id: randomUUID$1(), text, createdAt: Date.now(), alreadyStored: true }]
22351
+ messageQueue: [...existingQueue, { id: randomUUID(), text, createdAt: Date.now(), alreadyStored: true }]
21490
22352
  };
21491
22353
  sessionService.updateMetadata(sessionMetadata);
21492
22354
  };
@@ -21743,11 +22605,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
21743
22605
  });
21744
22606
  },
21745
22607
  onIssue: async (params) => {
21746
- const { issueRpc } = await import('./rpc-Cx6KsBYC.mjs');
22608
+ const { issueRpc } = await import('./rpc-TXF-di9u.mjs');
21747
22609
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
21748
22610
  },
21749
22611
  onWorkflow: async (params) => {
21750
- const { workflowRpc } = await import('./rpc-CclCeDcu.mjs');
22612
+ const { workflowRpc } = await import('./rpc-FGp88Yxp.mjs');
21751
22613
  return workflowRpc(params?.cwd || directory, params || {});
21752
22614
  },
21753
22615
  onRipgrep: async (args, cwd) => {
@@ -22285,7 +23147,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
22285
23147
  });
22286
23148
  }
22287
23149
  };
22288
- pidToTrackedSession.set(randomUUID$1(), trackedSession);
23150
+ pidToTrackedSession.set(randomUUID(), trackedSession);
22289
23151
  let acpInboxReconcileTimer = null;
22290
23152
  const reconcileAcpIdleInbox = () => {
22291
23153
  if (acpStopped) {
@@ -22730,7 +23592,15 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
22730
23592
  };
22731
23593
  writeDaemonStateFile(localState);
22732
23594
  logger.log("Daemon state file written \u2014 daemon is up; restoring mounts/sessions in background");
22733
- serveManager$1.restore().catch((err) => logger.error(`[serve] mount restore failed: ${err?.message || err}`));
23595
+ serveManager$1.restore().then(async () => {
23596
+ try {
23597
+ for (const m of serveManager$1.listMounts()) {
23598
+ if (m?.sessionId && m?.url) await reconcileMountSessionLink(m.sessionId, m.name, m.url);
23599
+ }
23600
+ } catch (err) {
23601
+ logger.log(`[serve] mount link reconcile failed: ${err?.message || err}`);
23602
+ }
23603
+ }).catch((err) => logger.error(`[serve] mount restore failed: ${err?.message || err}`));
22734
23604
  const daemonOwnerEmail = parseJwtEmail(process.env.HYPHA_TOKEN || "") || null;
22735
23605
  serveManager$1.setSessionResolver((sessionId) => {
22736
23606
  for (const [, session] of pidToTrackedSession) {
@@ -22750,7 +23620,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
22750
23620
  }
22751
23621
  return null;
22752
23622
  });
22753
- const sessionFilesToken = createHash$1("sha256").update(`session-files:${machineId}`).digest("hex").slice(0, 20);
23623
+ const sessionFilesToken = createHash("sha256").update(`session-files:${machineId}`).digest("hex").slice(0, 20);
22754
23624
  serveManager$1.ensureSessionFilesGateway(sessionFilesToken).then((base) => logger.log(`[serve] session-files gateway: ${base || "(tunnel pending)"}`)).catch((err) => logger.error(`[serve] session-files gateway failed: ${err?.message || err}`));
22755
23625
  const persistedSessions = loadPersistedSessions();
22756
23626
  try {
@@ -22790,7 +23660,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
22790
23660
  }
22791
23661
  if (persistedSessions.length > 0) {
22792
23662
  try {
22793
- const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-CnCpensV.mjs');
23663
+ const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-dbXgsGNK.mjs');
22794
23664
  await awaitClaudeVersionReady();
22795
23665
  } catch {
22796
23666
  }
@@ -23035,7 +23905,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23035
23905
  const PING_TIMEOUT_MS = 15e3;
23036
23906
  const POST_RECONNECT_GRACE_MS = 2e4;
23037
23907
  const RECONNECT_JITTER_MS = 2500;
23038
- const { WorkflowScheduler } = await import('./scheduler-Wzzed1CU.mjs');
23908
+ const { WorkflowScheduler } = await import('./scheduler-b-ysyKIj.mjs');
23039
23909
  const workflowProjectRoots = () => {
23040
23910
  const dirs = /* @__PURE__ */ new Set();
23041
23911
  for (const s of pidToTrackedSession.values()) {
@@ -23669,4 +24539,4 @@ var run = /*#__PURE__*/Object.freeze({
23669
24539
  writeStopMarker: writeStopMarker
23670
24540
  });
23671
24541
 
23672
- export { SKILLS_DIR as $, setWorkflowEnabled as A, removeWorkflow as B, validateWorkflowName as C, saveWorkflow as D, rawWorkflow as E, listWorkflows as F, isWorkflowEnabled as G, workflowSchedules as H, inZone as I, cronMatches as J, summarize as K, workflowSteps as L, parseJwtEmail as M, computeCollectionConfigUpdate as N, SYSTEM_COLLECTION_CONFIG as O, loadMachineContext as P, buildMachineInstructions as Q, READ_ONLY_TOOLS as R, SharingNotificationSync as S, machineToolsForRole as T, buildMachineTools as U, parseFrontmatter as V, getSkillsServer as W, getSkillsWorkspaceName as X, getSkillsCollectionName as Y, fetchWithTimeout as Z, searchSkills as _, createSessionStore as a, getSkillInfo as a0, downloadSkillFile as a1, listSkillFiles as a2, resolveModel as a3, clearStopMarker as a4, stopMarkerExists as a5, formatHandle as a6, normalizeAllowedUser as a7, loadSecurityContextConfig as a8, resolveSecurityContext as a9, KimiTransport$1 as aA, kimiProvider as aB, kimiSetup as aC, api as aD, run as aE, buildSecurityContextFromFlags as aa, mergeSecurityContexts as ab, buildSessionShareUrl as ac, computeOutboundHop as ad, registerAwaitingReply as ae, buildMachineShareUrl as af, parseHandle as ag, handleMatchesMetadata as ah, PINNED_CODEX_VERSION as ai, withFileLock as aj, describeMisconfiguration as ak, buildMachineDeps as al, applyClaudeProxyEnv as am, composeSessionId as an, generateFriendlyName as ao, generateHookSettings as ap, staticFileServer as aq, instanceConfig as ar, claudeAuth as as, codexProvider as at, projectInfo as au, DefaultTransport$1 as av, acpBackend as aw, acpAgentConfig as ax, codexAppServerBackend as ay, GeminiTransport$1 as az, stopDaemon as b, connectToHypha as c, daemonStatus as d, getFrpsSubdomainHost as e, getFrpsServerPort as f, getHyphaServerUrl$1 as g, getFrpsServerAddr as h, shortId as i, resolveProjectRoot as j, getIssue as k, resumeIssue as l, addComment as m, addIssue as n, listIssues as o, pauseIssue as p, isPendingIssue as q, registerMachineService as r, startDaemon as s, searchIssues as t, updateIssue as u, isVisibleTo as v, getRun as w, listRuns as x, getWorkflow as y, runWorkflow as z };
24542
+ export { listSkillFiles as $, saveWorkflow as A, rawWorkflow as B, listWorkflows as C, isWorkflowEnabled as D, workflowSchedules as E, inZone as F, cronMatches as G, summarize as H, workflowSteps as I, parseJwtEmail as J, computeCollectionConfigUpdate as K, SYSTEM_COLLECTION_CONFIG as L, loadMachineContext as M, buildMachineInstructions as N, machineToolsForRole as O, buildMachineTools as P, parseFrontmatter as Q, READ_ONLY_TOOLS as R, SharingNotificationSync as S, getSkillsServer as T, getSkillsWorkspaceName as U, getSkillsCollectionName as V, fetchWithTimeout as W, searchSkills as X, SKILLS_DIR as Y, getSkillInfo as Z, downloadSkillFile as _, createSessionStore as a, resolveModel as a0, clearStopMarker as a1, stopMarkerExists as a2, formatHandle as a3, normalizeAllowedUser as a4, loadSecurityContextConfig as a5, resolveSecurityContext as a6, buildSecurityContextFromFlags as a7, mergeSecurityContexts as a8, buildSessionShareUrl as a9, kimiSetup as aA, api as aB, run as aC, computeOutboundHop as aa, registerAwaitingReply as ab, buildMachineShareUrl as ac, parseHandle as ad, handleMatchesMetadata as ae, PINNED_CODEX_VERSION as af, withFileLock as ag, describeMisconfiguration as ah, buildMachineDeps as ai, applyClaudeProxyEnv as aj, composeSessionId as ak, generateFriendlyName as al, generateHookSettings as am, instanceConfig as an, frpc as ao, staticFileServer as ap, claudeAuth as aq, codexProvider as ar, projectInfo as as, DefaultTransport$1 as at, acpBackend as au, acpAgentConfig as av, codexAppServerBackend as aw, GeminiTransport$1 as ax, KimiTransport$1 as ay, kimiProvider as az, stopDaemon as b, connectToHypha as c, daemonStatus as d, shortId as e, resolveProjectRoot as f, getHyphaServerUrl$1 as g, getIssue as h, resumeIssue as i, addComment as j, addIssue as k, listIssues as l, isPendingIssue as m, searchIssues as n, isVisibleTo as o, pauseIssue as p, getRun as q, registerMachineService as r, startDaemon as s, listRuns as t, updateIssue as u, getWorkflow as v, runWorkflow as w, setWorkflowEnabled as x, removeWorkflow as y, validateWorkflowName as z };