@agent-team-foundation/first-tree-hub 0.10.10 → 0.10.11

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.
@@ -585,11 +585,14 @@ const serverConfigSchema = defineConfig({
585
585
  })
586
586
  }) }),
587
587
  cors: optional({ origin: field(z.string(), { env: "FIRST_TREE_HUB_CORS_ORIGIN" }) }),
588
+ trustProxy: field(z.boolean().default(false), { env: "FIRST_TREE_HUB_TRUST_PROXY" }),
588
589
  rateLimit: optional({
589
590
  max: field(z.number().default(100), { env: "FIRST_TREE_HUB_RATE_LIMIT_MAX" }),
590
591
  loginMax: field(z.number().default(5), { env: "FIRST_TREE_HUB_RATE_LIMIT_LOGIN_MAX" }),
591
- webhookMax: field(z.number().default(60), { env: "FIRST_TREE_HUB_RATE_LIMIT_WEBHOOK_MAX" })
592
+ webhookMax: field(z.number().default(60), { env: "FIRST_TREE_HUB_RATE_LIMIT_WEBHOOK_MAX" }),
593
+ agentMessageMax: field(z.number().default(30), { env: "FIRST_TREE_HUB_RATE_LIMIT_AGENT_MESSAGE_MAX" })
592
594
  }),
595
+ ws: optional({ maxPayload: field(z.number().int().min(1024).default(262144), { env: "FIRST_TREE_HUB_WS_MAX_PAYLOAD" }) }),
593
596
  inbox: optional({ maxInFlightPerAgent: field(z.number().int().min(1).max(1024).default(32), { env: "FIRST_TREE_HUB_INBOX_MAX_IN_FLIGHT_PER_AGENT" }) }),
594
597
  kael: optional({
595
598
  endpoint: field(z.string(), { env: "KAEL_ENDPOINT" }),
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import "../observability-DPyf745N-BSc8QNcR.mjs";
3
- import { $ as success, A as checkServerHealth, C as checkAgentConfigs, D as checkDocker, E as checkDatabase, F as getClientServiceStatus, H as stopPostgres, I as installClientService, J as removeLocalAgent, K as findStaleAliases, L as isServiceSupported, M as checkWebSocket, N as printResults, O as checkNodeVersion, P as reconcileAgentConfigs, Q as fail, S as runMigrations, T as checkClientConfig, U as ClientRuntime, W as handleClientOrgMismatch, Y as createOwner, Z as resolveReplyToFromEnv, _ as onboardCreate, a as declineUpdate, at as ClientUserMismatchError, b as createApiNameResolver, c as COMMAND_VERSION, ct as SessionRegistry, d as isInteractive, dt as applyClientLoggerConfig, f as promptAddAgent, ft as configureClientLoggerForService, g as onboardCheck, h as loadOnboardState, i as createExecuteUpdate, it as ClientOrgMismatchError, j as checkServerReachable, k as checkServerConfig, l as reconcileLocalRuntimeProviders, lt as cleanWorkspaces, m as formatCheckReport, nt as setJsonMode, o as promptUpdate, ot as FirstTreeHubSDK, p as promptMissingFields, q as formatStaleReason, r as registerSaaSConnectCommand, s as startServer, st as SdkError, tt as print, u as uploadClientCapabilities, ut as probeCapabilities, v as saveOnboardState, w as checkBackgroundService, x as migrateLocalAgentDirs, y as runHomeMigration } from "../saas-connect-CuJWyzzq.mjs";
3
+ import { $ as findStaleAliases, A as checkDatabase, B as installClientService, C as runHomeMigration, D as checkAgentConfigs, E as runMigrations, F as checkServerReachable, G as stopClientService, I as checkWebSocket, L as printResults, M as checkNodeVersion, N as checkServerConfig, O as checkBackgroundService, P as checkServerHealth, R as reconcileAgentConfigs, S as saveOnboardState, T as migrateLocalAgentDirs, U as restartClientService, V as isServiceSupported, W as startClientService, X as ClientRuntime, Y as stopPostgres, Z as handleClientOrgMismatch, _ as promptMissingFields, _t as probeCapabilities, a as declineUpdate, at as fail, b as onboardCheck, c as detectInstallMode, ct as print, d as startServer, dt as ClientOrgMismatchError, et as formatStaleReason, f as COMMAND_VERSION, ft as ClientUserMismatchError, g as promptAddAgent, gt as cleanWorkspaces, h as isInteractive, ht as SessionRegistry, i as createExecuteUpdate, it as resolveReplyToFromEnv, j as checkDocker, k as checkClientConfig, l as fetchLatestVersion, lt as setJsonMode, m as uploadClientCapabilities, mt as SdkError, nt as createOwner, o as promptUpdate, ot as success, p as reconcileLocalRuntimeProviders, pt as FirstTreeHubSDK, r as registerSaaSConnectCommand, s as PACKAGE_NAME, tt as removeLocalAgent, u as installGlobalLatest, v as formatCheckReport, vt as applyClientLoggerConfig, w as createApiNameResolver, x as onboardCreate, y as loadOnboardState, yt as configureClientLoggerForService, z as getClientServiceStatus } from "../saas-connect-D-7x9KRd.mjs";
4
4
  import "../logger-core-BTmvdflj-DjW8FM4T.mjs";
5
- import { C as serverConfigSchema, S as resolveConfigReadonly, _ as loadAgents, b as resetConfig, c as saveCredentials, d as DEFAULT_HOME_DIR, f as agentConfigSchema, g as initConfig, h as getConfigValue, i as loadCredentials, l as DEFAULT_CONFIG_DIR, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, r as ensureFreshAdminToken, s as saveAgentConfig, u as DEFAULT_DATA_DIR, w as setConfigValue, x as resetConfigMeta, y as readConfigFile } from "../bootstrap-jx5nN1qZ.mjs";
5
+ import { C as serverConfigSchema, S as resolveConfigReadonly, _ as loadAgents, b as resetConfig, c as saveCredentials, d as DEFAULT_HOME_DIR, f as agentConfigSchema, g as initConfig, h as getConfigValue, i as loadCredentials, l as DEFAULT_CONFIG_DIR, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, r as ensureFreshAdminToken, s as saveAgentConfig, u as DEFAULT_DATA_DIR, w as setConfigValue, x as resetConfigMeta, y as readConfigFile } from "../bootstrap-BTKiVIYk.mjs";
6
6
  import "../dist-DwbhZyGi.mjs";
7
7
  import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-viiZmwcn.mjs";
8
8
  import "../invitation-B1pjAyOz-BaCA9PII.mjs";
9
9
  import { join } from "node:path";
10
10
  import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
11
+ import * as semver from "semver";
11
12
  import { Command } from "commander";
12
13
  import { confirm, input, password, select } from "@inquirer/prompts";
13
14
  //#region src/commands/agent-config.ts
@@ -1081,8 +1082,37 @@ function registerConnectCommand(parent) {
1081
1082
  function registerClientCommands(program) {
1082
1083
  const client = program.command("client").description("Client runtime — connect agents to the server");
1083
1084
  registerConnectCommand(client);
1084
- client.command("start").description("Start client — connect all configured agents to the server").option("--no-interactive", "Skip interactive prompts (for Docker/CI)").action(async (options) => {
1085
+ client.command("start").description("Start client — connect all configured agents to the server").option("--no-interactive", "Skip interactive prompts (for Docker/CI)").option("--foreground", "Run inline instead of delegating to the background service (for debugging)").action(async (options) => {
1085
1086
  try {
1087
+ const isSupervisorChild = options.interactive === false && process.env.FIRST_TREE_HUB_SERVICE_MODE === "1";
1088
+ if (!(options.foreground === true || isSupervisorChild) && isServiceSupported()) {
1089
+ const svc = getClientServiceStatus();
1090
+ if (svc.state === "active") {
1091
+ print.line("\n");
1092
+ print.line(` Service is already running (${svc.platform}${svc.detail ? `, ${svc.detail}` : ""}).\n`);
1093
+ print.line(" Use `first-tree-hub client restart` to restart, or `--foreground` to run inline.\n\n");
1094
+ return;
1095
+ }
1096
+ if (svc.state === "inactive") {
1097
+ const res = startClientService();
1098
+ if (!res.ok) {
1099
+ print.line(`\n Failed to start service: ${res.reason}\n`);
1100
+ print.line(" Try `--foreground` to run inline instead.\n\n");
1101
+ process.exit(1);
1102
+ }
1103
+ const after = getClientServiceStatus();
1104
+ print.line("\n");
1105
+ print.line(` Started ${after.platform} service${after.detail ? ` (${after.detail})` : ""}.\n`);
1106
+ const journalHint = after.platform === "systemd" ? ` (or \`journalctl --user -u ${after.label.replace(/\.service$/, "")}\`)` : "";
1107
+ print.line(` Logs: ${after.logDir}${journalHint}\n\n`);
1108
+ return;
1109
+ }
1110
+ if (svc.state === "unknown") {
1111
+ print.line(`\n Service state could not be determined (${svc.platform}${svc.detail ? `: ${svc.detail}` : ""}).\n`);
1112
+ print.line(" Inspect with `first-tree-hub client doctor`, or pass `--foreground` to bypass.\n\n");
1113
+ process.exit(1);
1114
+ }
1115
+ }
1086
1116
  await promptMissingFields({
1087
1117
  schema: clientConfigSchema,
1088
1118
  role: "client",
@@ -1212,11 +1242,72 @@ function registerClientCommands(program) {
1212
1242
  checkBackgroundService()
1213
1243
  ]);
1214
1244
  });
1215
- client.command("stop").description("Stop the client (sends SIGTERM to running process)").action(() => {
1216
- print.line(" Client stop: use Ctrl+C or `kill` the running process.\n");
1217
- print.line(" Daemon mode with PID file is planned for a future release.\n");
1245
+ client.command("stop").description("Stop the background service (preserves auto-start; use `client start` to bring it back)").action(() => {
1246
+ if (!isServiceSupported()) {
1247
+ print.line(`\n Service control not supported on ${process.platform}.\n`);
1248
+ print.line(" If running inline, use Ctrl+C or kill the process.\n\n");
1249
+ return;
1250
+ }
1251
+ const svc = getClientServiceStatus();
1252
+ if (svc.state === "not-installed") {
1253
+ print.line("\n No background service installed — nothing to stop.\n");
1254
+ print.line(" If running inline, use Ctrl+C or kill the process.\n\n");
1255
+ return;
1256
+ }
1257
+ if (svc.state === "inactive") {
1258
+ print.line("\n Service is already stopped.\n\n");
1259
+ return;
1260
+ }
1261
+ const res = stopClientService();
1262
+ if (!res.ok) {
1263
+ print.line(`\n Failed to stop service: ${res.reason}\n\n`);
1264
+ process.exit(1);
1265
+ }
1266
+ print.line(`\n Stopped ${svc.platform} service.\n`);
1267
+ print.line(" Auto-start on next login is preserved. Run `first-tree-hub client start` to bring it back.\n\n");
1268
+ });
1269
+ client.command("restart").description("Restart the background service").action(() => {
1270
+ if (!isServiceSupported()) {
1271
+ print.line(`\n Service control not supported on ${process.platform}.\n`);
1272
+ print.line(" Restart your inline `client start` process manually.\n\n");
1273
+ return;
1274
+ }
1275
+ if (getClientServiceStatus().state === "not-installed") {
1276
+ print.line("\n No background service installed.\n");
1277
+ print.line(" Run `first-tree-hub client connect <url>` first.\n\n");
1278
+ process.exit(1);
1279
+ }
1280
+ const res = restartClientService();
1281
+ if (!res.ok) {
1282
+ print.line(`\n Failed to restart service: ${res.reason}\n\n`);
1283
+ process.exit(1);
1284
+ }
1285
+ const after = getClientServiceStatus();
1286
+ print.line(`\n Restarted ${after.platform} service${after.detail ? ` (${after.detail})` : ""}.\n\n`);
1218
1287
  });
1219
- client.command("status").description("Show client and agent connection status").action(() => {
1288
+ client.command("status").description("Show CLI, service, hub, and agent status (one-screen overview)").action(() => {
1289
+ print.line("\n");
1290
+ print.line(` CLI: ${COMMAND_VERSION}\n`);
1291
+ if (isServiceSupported()) {
1292
+ const svc = getClientServiceStatus();
1293
+ const tail = svc.platform === "systemd" ? ` (logs: journalctl --user -u ${svc.label.replace(/\.service$/, "")} -f)` : "";
1294
+ if (svc.state === "active") print.line(` Service: ✓ running (${svc.platform}${svc.detail ? `, ${svc.detail}` : ""})${tail}\n`);
1295
+ else if (svc.state === "inactive") print.line(` Service: ✗ stopped (${svc.platform}${svc.detail ? `, ${svc.detail}` : ""})\n`);
1296
+ else if (svc.state === "not-installed") print.line(" Service: not installed — run `first-tree-hub client connect <url>`\n");
1297
+ else print.line(` Service: unknown (${svc.platform}${svc.detail ? `, ${svc.detail}` : ""})\n`);
1298
+ } else print.line(` Service: not supported on ${process.platform} (runs inline)\n`);
1299
+ const clientYaml = join(DEFAULT_CONFIG_DIR, "client.yaml");
1300
+ if (existsSync(clientYaml)) try {
1301
+ const cfg = readConfigFile(clientYaml);
1302
+ const serverUrl = getNested(cfg, "server.url");
1303
+ const clientId = getNested(cfg, "client.id");
1304
+ print.line(` Hub: ${serverUrl ?? "(not configured)"}\n`);
1305
+ print.line(` Client: ${clientId ?? "(not configured)"}\n`);
1306
+ } catch (err) {
1307
+ const msg = err instanceof Error ? err.message : String(err);
1308
+ print.line(` Hub: (could not read ${clientYaml}: ${msg.slice(0, 60)})\n`);
1309
+ }
1310
+ else print.line(" Hub: (not configured — run `first-tree-hub client connect <url>`)\n");
1220
1311
  const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
1221
1312
  try {
1222
1313
  const agents = loadAgents({
@@ -1224,14 +1315,14 @@ function registerClientCommands(program) {
1224
1315
  agentsDir
1225
1316
  });
1226
1317
  if (agents.size === 0) {
1227
- print.line(" No agents configured.\n");
1318
+ print.line(" Agents: 0 configured\n\n");
1228
1319
  return;
1229
1320
  }
1230
- print.line("\n Configured agents:\n\n");
1231
- for (const [name, config] of agents) print.line(` ${name.padEnd(20)} runtime: ${config.runtime.padEnd(14)} agentId: ${config.agentId}\n`);
1321
+ print.line(` Agents: ${agents.size} configured\n\n`);
1322
+ for (const [name, config] of agents) print.line(` ${name.padEnd(20)} runtime: ${config.runtime.padEnd(14)} agentId: ${config.agentId}\n`);
1232
1323
  print.line("\n");
1233
1324
  } catch {
1234
- print.line(" No agents directory found.\n");
1325
+ print.line(" Agents: (no agents directory)\n\n");
1235
1326
  }
1236
1327
  });
1237
1328
  client.command("hub-list").description("List clients on the Hub server").option("--server <url>", "Hub server URL").action(async (options) => {
@@ -1374,6 +1465,15 @@ function timeSince(isoDate) {
1374
1465
  if (hours < 24) return `${hours}h ${minutes % 60}m`;
1375
1466
  return `${Math.floor(hours / 24)}d ${hours % 24}h`;
1376
1467
  }
1468
+ /** Read a `dot.path.like.this` from a parsed YAML object, returning string | null. */
1469
+ function getNested(obj, path) {
1470
+ let cur = obj;
1471
+ for (const part of path.split(".")) {
1472
+ if (cur === null || cur === void 0 || typeof cur !== "object") return null;
1473
+ cur = cur[part];
1474
+ }
1475
+ return typeof cur === "string" ? cur : null;
1476
+ }
1377
1477
  //#endregion
1378
1478
  //#region src/commands/config.ts
1379
1479
  function resolveConfigPath(flags) {
@@ -1471,13 +1571,13 @@ function isSecretField(schema, dotPath) {
1471
1571
  //#region src/commands/onboard.ts
1472
1572
  async function promptMissing(args) {
1473
1573
  if (!args.server) try {
1474
- const { resolveServerUrl } = await import("../bootstrap-jx5nN1qZ.mjs").then((n) => n.t);
1574
+ const { resolveServerUrl } = await import("../bootstrap-BTKiVIYk.mjs").then((n) => n.t);
1475
1575
  resolveServerUrl();
1476
1576
  } catch {
1477
1577
  args.server = await input({ message: "Hub server URL:" });
1478
1578
  saveOnboardState(args);
1479
1579
  }
1480
- const { loadCredentials } = await import("../bootstrap-jx5nN1qZ.mjs").then((n) => n.t);
1580
+ const { loadCredentials } = await import("../bootstrap-BTKiVIYk.mjs").then((n) => n.t);
1481
1581
  if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub client connect <server-url>` before onboarding.");
1482
1582
  if (!args.id) {
1483
1583
  args.id = await input({ message: "Agent ID:" });
@@ -1679,62 +1779,92 @@ function registerServerCommands(program) {
1679
1779
  });
1680
1780
  }
1681
1781
  //#endregion
1682
- //#region src/commands/status.ts
1683
- function registerStatusCommand(program) {
1684
- program.command("status").description("Global overviewserver health + configured agents").action(async () => {
1685
- print.line("\n");
1686
- const serverConfig = readConfigFile(join(DEFAULT_CONFIG_DIR, "server.yaml"));
1687
- const serverPort = getNestedValue(serverConfig, "server.port") ?? 8e3;
1688
- const serverUrl = `http://${getNestedValue(serverConfig, "server.host") ?? "127.0.0.1"}:${serverPort}`;
1782
+ //#region src/commands/update.ts
1783
+ /**
1784
+ * `first-tree-hub update` user-driven CLI upgrade.
1785
+ *
1786
+ * Lives at the top level (not under `client`) because the tarball bundles
1787
+ * server / client / web / shared into a single artifact: upgrading affects
1788
+ * the whole CLI, not the client subsystem alone.
1789
+ *
1790
+ * Pairs with — but does not replace — the server-driven UpdateManager
1791
+ * (packages/client/src/runtime/update-manager.ts), which fires automatically
1792
+ * when a connected client falls behind the server-bundled version. This
1793
+ * command is the manual equivalent: same install + restart sequence, but
1794
+ * triggered on the operator's terms.
1795
+ */
1796
+ function registerUpdateCommand(program) {
1797
+ program.command("update").description("Upgrade first-tree-hub to the latest published version and restart the service").option("--check", "Only check whether a newer version is available; do not install").option("--no-restart", "Install the new version but skip restarting the background service").action(async (options) => {
1798
+ const mode = detectInstallMode();
1799
+ if (mode === "source") {
1800
+ print.line("\n Running from a source checkout — `update` is a no-op.\n");
1801
+ print.line(" Use `git pull` instead.\n\n");
1802
+ return;
1803
+ }
1804
+ if (mode === "npx") {
1805
+ print.line("\n Not launched from a global npm install — cannot self-update.\n");
1806
+ print.line(` Install globally first: npm i -g ${PACKAGE_NAME}\n\n`);
1807
+ return;
1808
+ }
1809
+ print.line("\n Checking npm registry...\n");
1810
+ const latest = fetchLatestVersion();
1811
+ if (!latest.ok) {
1812
+ print.line(` Could not fetch latest version: ${latest.reason}\n\n`);
1813
+ process.exit(1);
1814
+ }
1815
+ const current = COMMAND_VERSION;
1816
+ if ((semver.valid(current) ? semver.compare(current, latest.version) : -1) >= 0) {
1817
+ print.line(` Already on ${current} (latest is ${latest.version}).\n\n`);
1818
+ return;
1819
+ }
1820
+ if (options.check) {
1821
+ print.line(` Update available: ${current} → ${latest.version}\n`);
1822
+ print.line(" Run `first-tree-hub update` to install.\n\n");
1823
+ return;
1824
+ }
1825
+ print.line(` Updating ${current} → ${latest.version}...\n`);
1826
+ const installRes = await installGlobalLatest();
1827
+ if (!installRes.ok) {
1828
+ print.line(`\n Install failed: ${installRes.reason}\n\n`);
1829
+ process.exit(1);
1830
+ }
1831
+ const installed = installRes.installedVersion ?? latest.version;
1832
+ print.line(` Installed ${installed}.\n`);
1833
+ if (options.restart === false) {
1834
+ print.line(" Skipping restart (--no-restart). Run `first-tree-hub client restart` when ready.\n\n");
1835
+ return;
1836
+ }
1837
+ if (!isServiceSupported()) {
1838
+ print.line(` No service manager on ${process.platform}; restart your inline `);
1839
+ print.line("`client start` process to pick up the new version.\n\n");
1840
+ return;
1841
+ }
1842
+ const svc = getClientServiceStatus();
1843
+ if (svc.state === "not-installed") {
1844
+ print.line(" No background service installed — nothing to restart.\n");
1845
+ print.line(" Run `first-tree-hub client connect <url>` to set one up.\n\n");
1846
+ return;
1847
+ }
1689
1848
  try {
1690
- const res = await fetch(`${serverUrl}/api/v1/health`);
1691
- if (res.ok) {
1692
- const data = await res.json();
1693
- const uptime = data.uptime_seconds ? formatUptime(data.uptime_seconds) : "unknown";
1694
- print.line(` Server: ✓ running (${serverUrl}, uptime: ${uptime})\n`);
1695
- } else print.line(` Server: ✗ unhealthy (${res.status})\n`);
1696
- } catch {
1697
- print.line(` Server: ✗ not running (${serverUrl})\n`);
1849
+ installClientService();
1850
+ } catch (err) {
1851
+ const msg = err instanceof Error ? err.message : String(err);
1852
+ print.line(` warning: unit-file refresh failed: ${msg}\n`);
1853
+ print.line(" Continuing with restart against the old unit.\n");
1698
1854
  }
1699
- const dbProvider = getNestedValue(serverConfig, "database.provider") ?? "unknown";
1700
- const hasDbUrl = getNestedValue(serverConfig, "database.url") !== void 0;
1701
- print.line(` Database: ${hasDbUrl ? "✓ configured" : "✗ not configured"} (${dbProvider})\n`);
1702
- const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
1703
- if (existsSync(agentsDir)) try {
1704
- const agents = loadAgents({
1705
- schema: agentConfigSchema,
1706
- agentsDir
1707
- });
1708
- print.line(` Agents: ${agents.size} configured\n`);
1709
- } catch {
1710
- print.line(" Agents: error reading config\n");
1711
- }
1712
- else print.line(" Agents: 0 configured\n");
1713
- const clientConfigPath = join(DEFAULT_CONFIG_DIR, "client.yaml");
1714
- if (existsSync(clientConfigPath)) {
1715
- const clientServerUrl = getNestedValue(readConfigFile(clientConfigPath), "server.url");
1716
- print.line(` Client: configured → ${clientServerUrl}\n`);
1717
- } else print.line(" Client: not configured\n");
1718
- print.line("\n");
1855
+ if (svc.state === "inactive") {
1856
+ print.line(" Service is stopped leaving it stopped. Use `client start` to bring it up.\n\n");
1857
+ return;
1858
+ }
1859
+ const restartRes = restartClientService();
1860
+ if (!restartRes.ok) {
1861
+ print.line(`\n Service restart failed: ${restartRes.reason}\n`);
1862
+ print.line(" Run `first-tree-hub client restart` to retry.\n\n");
1863
+ process.exit(1);
1864
+ }
1865
+ print.line(` Service restarted on ${installed}.\n\n`);
1719
1866
  });
1720
1867
  }
1721
- function getNestedValue(obj, dotPath) {
1722
- const parts = dotPath.split(".");
1723
- let current = obj;
1724
- for (const part of parts) {
1725
- if (current === null || current === void 0 || typeof current !== "object") return void 0;
1726
- current = current[part];
1727
- }
1728
- return current;
1729
- }
1730
- function formatUptime(seconds) {
1731
- const days = Math.floor(seconds / 86400);
1732
- const hours = Math.floor(seconds % 86400 / 3600);
1733
- const mins = Math.floor(seconds % 3600 / 60);
1734
- if (days > 0) return `${days}d ${hours}h`;
1735
- if (hours > 0) return `${hours}h ${mins}m`;
1736
- return `${mins}m`;
1737
- }
1738
1868
  //#endregion
1739
1869
  //#region src/cli/index.ts
1740
1870
  runHomeMigration();
@@ -1759,7 +1889,7 @@ registerServerCommands(program);
1759
1889
  registerClientCommands(program);
1760
1890
  registerAgentCommands(program);
1761
1891
  registerConfigCommands(program);
1762
- registerStatusCommand(program);
1892
+ registerUpdateCommand(program);
1763
1893
  registerOnboardCommand(program);
1764
1894
  program.parse();
1765
1895
  //#endregion
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import "./observability-DPyf745N-BSc8QNcR.mjs";
2
- import { A as checkServerHealth, B as ensurePostgres, C as checkAgentConfigs, D as checkDocker, E as checkDatabase, F as getClientServiceStatus, G as rotateClientIdWithBackup, H as stopPostgres, I as installClientService, L as isServiceSupported, M as checkWebSocket, N as printResults, O as checkNodeVersion, R as resolveCliInvocation, S as runMigrations, T as checkClientConfig, U as ClientRuntime, V as isDockerAvailable, W as handleClientOrgMismatch, X as hasUser, Y as createOwner, _ as onboardCreate, d as isInteractive, et as blank, f as promptAddAgent, g as onboardCheck, j as checkServerReachable, k as checkServerConfig, m as formatCheckReport, n as deriveHubUrlFromToken, ot as FirstTreeHubSDK, p as promptMissingFields, rt as status, s as startServer, st as SdkError, t as HubUrlDerivationError, y as runHomeMigration, z as uninstallClientService } from "./saas-connect-CuJWyzzq.mjs";
2
+ import { A as checkDatabase, B as installClientService, C as runHomeMigration, D as checkAgentConfigs, E as runMigrations, F as checkServerReachable, G as stopClientService, H as resolveCliInvocation, I as checkWebSocket, J as isDockerAvailable, K as uninstallClientService, L as printResults, M as checkNodeVersion, N as checkServerConfig, P as checkServerHealth, Q as rotateClientIdWithBackup, U as restartClientService, V as isServiceSupported, W as startClientService, X as ClientRuntime, Y as stopPostgres, Z as handleClientOrgMismatch, _ as promptMissingFields, b as onboardCheck, d as startServer, g as promptAddAgent, h as isInteractive, j as checkDocker, k as checkClientConfig, mt as SdkError, n as deriveHubUrlFromToken, nt as createOwner, pt as FirstTreeHubSDK, q as ensurePostgres, rt as hasUser, st as blank, t as HubUrlDerivationError, ut as status, v as formatCheckReport, x as onboardCreate, z as getClientServiceStatus } from "./saas-connect-D-7x9KRd.mjs";
3
3
  import "./logger-core-BTmvdflj-DjW8FM4T.mjs";
4
- import { a as resolveAccessToken, n as ensureFreshAccessToken, o as resolveServerUrl, r as ensureFreshAdminToken } from "./bootstrap-jx5nN1qZ.mjs";
4
+ import { a as resolveAccessToken, n as ensureFreshAccessToken, o as resolveServerUrl, r as ensureFreshAdminToken } from "./bootstrap-BTKiVIYk.mjs";
5
5
  import "./dist-DwbhZyGi.mjs";
6
6
  import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-viiZmwcn.mjs";
7
7
  import "./invitation-B1pjAyOz-BaCA9PII.mjs";
8
- export { ClientRuntime, FirstTreeHubSDK, HubUrlDerivationError, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, deriveHubUrlFromToken, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, getClientServiceStatus, handleClientOrgMismatch, hasUser, installClientService, isDockerAvailable, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, rotateClientIdWithBackup, runHomeMigration, runMigrations, startServer, status, stopPostgres, uninstallClientService };
8
+ export { ClientRuntime, FirstTreeHubSDK, HubUrlDerivationError, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, deriveHubUrlFromToken, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, getClientServiceStatus, handleClientOrgMismatch, hasUser, installClientService, isDockerAvailable, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, restartClientService, rotateClientIdWithBackup, runHomeMigration, runMigrations, startClientService, startServer, status, stopClientService, stopPostgres, uninstallClientService };
@@ -1,11 +1,11 @@
1
1
  import { m as __toESM } from "./esm-CYu4tXXn.mjs";
2
2
  import { _ as withSpan, a as endWsConnectionSpan, b as require_pino, c as messageAttrs, d as rootLogger$1, g as startWsConnectionSpan, i as currentTraceId, n as applyLoggerConfig, o as getFastifyOtelPlugin, p as setWsConnectionAttrs, r as createLogger$1, t as adapterAttrs, u as observabilityPlugin, v as withWsMessageSpan, y as FIRST_TREE_HUB_ATTR } from "./observability-DPyf745N-BSc8QNcR.mjs";
3
- import { C as serverConfigSchema, S as resolveConfigReadonly, _ as loadAgents, b as resetConfig, c as saveCredentials, d as DEFAULT_HOME_DIR$1, f as agentConfigSchema, g as initConfig, i as loadCredentials, l as DEFAULT_CONFIG_DIR, m as collectMissingPrompts, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, s as saveAgentConfig, u as DEFAULT_DATA_DIR$1, v as migrateLegacyHome, w as setConfigValue, x as resetConfigMeta } from "./bootstrap-jx5nN1qZ.mjs";
3
+ import { C as serverConfigSchema, S as resolveConfigReadonly, _ as loadAgents, b as resetConfig, c as saveCredentials, d as DEFAULT_HOME_DIR$1, f as agentConfigSchema, g as initConfig, i as loadCredentials, l as DEFAULT_CONFIG_DIR, m as collectMissingPrompts, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, s as saveAgentConfig, u as DEFAULT_DATA_DIR$1, v as migrateLegacyHome, w as setConfigValue, x as resetConfigMeta } from "./bootstrap-BTKiVIYk.mjs";
4
4
  import { $ as refreshTokenSchema, A as createOrgFromMeSchema, B as imageInlineContentSchema, C as clientRegisterSchema, D as createAgentSchema, E as createAdapterMappingSchema, F as dryRunAgentRuntimeConfigSchema, G as isReservedAgentName$1, H as inboxDeliverFrameSchema$1, I as extractMentions, J as loginSchema, K as joinByInvitationSchema, L as githubCallbackQuerySchema, M as createTaskSchema, N as defaultRuntimeConfigPayload, O as createChatSchema, P as delegateFeishuUserSchema, Q as rebindAgentSchema, R as githubDevCallbackQuerySchema, S as clientCapabilitiesSchema$1, St as wsAuthFrameSchema, T as createAdapterConfigSchema, U as inboxPollQuerySchema, V as inboxAckFrameSchema, W as isRedactedEnvValue, X as notificationQuerySchema, Y as messageSourceSchema$1, Z as paginationQuerySchema, _ as adminUpdateTaskSchema, _t as updateClientCapabilitiesSchema, a as AGENT_STATUSES, at as sendToAgentSchema, b as agentRuntimeConfigPayloadSchema$1, bt as updateSystemConfigSchema, ct as sessionEventSchema$1, d as TASK_HEALTH_SIGNALS, dt as switchOrgSchema, et as runtimeStateMessageSchema, f as TASK_STATUSES, ft as taskListQuerySchema, g as adminCreateTaskSchema, gt as updateChatSchema, h as addParticipantSchema, ht as updateAgentSchema, i as AGENT_SOURCES, it as sendMessageSchema, j as createOrganizationSchema, k as createMemberSchema, l as SYSTEM_CONFIG_DEFAULTS, lt as sessionReconcileRequestSchema, m as WS_AUTH_FRAME_TIMEOUT_MS, mt as updateAgentRuntimeConfigSchema, n as AGENT_NAME_REGEX$1, nt as scanMentionTokens, o as AGENT_TYPES, ot as sessionCompletionMessageSchema, p as TASK_TERMINAL_STATUSES, pt as updateAdapterConfigSchema, q as linkTaskChatSchema, r as AGENT_SELECTOR_HEADER$1, rt as selfServiceFeishuBotSchema, s as AGENT_VISIBILITY, st as sessionEventMessageSchema, t as AGENT_BIND_REJECT_REASONS, tt as safeRedirectPath, u as TASK_CREATOR_TYPES, ut as sessionStateMessageSchema, v as agentBindRequestSchema, vt as updateMemberSchema, w as connectTokenExchangeSchema, x as agentTypeSchema$1, xt as updateTaskStatusSchema, y as agentPinnedMessageSchema$1, yt as updateOrganizationSchema, z as githubStartQuerySchema } from "./dist-DwbhZyGi.mjs";
5
5
  import { _ as recordRedemption, a as ConflictError, b as uuidv7, c as UnauthorizedError, d as findActiveByToken, f as getActiveInvitation, h as organizations, i as ClientUserMismatchError$1, l as buildInviteUrl, m as invitations, n as BadRequestError, o as ForbiddenError, p as invitationRedemptions, r as ClientOrgMismatchError$1, s as NotFoundError, t as AppError, u as ensureActiveInvitation, y as users } from "./invitation-B1pjAyOz-BaCA9PII.mjs";
6
6
  import { createRequire } from "node:module";
7
7
  import { ZodError, z } from "zod";
8
- import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
8
+ import { basename, delimiter, dirname, isAbsolute, join, resolve } from "node:path";
9
9
  import { Writable } from "node:stream";
10
10
  import { homedir, hostname, platform, tmpdir, userInfo } from "node:os";
11
11
  import { EventEmitter } from "node:events";
@@ -1539,11 +1539,14 @@ defineConfig({
1539
1539
  })
1540
1540
  }) }),
1541
1541
  cors: optional({ origin: field(z.string(), { env: "FIRST_TREE_HUB_CORS_ORIGIN" }) }),
1542
+ trustProxy: field(z.boolean().default(false), { env: "FIRST_TREE_HUB_TRUST_PROXY" }),
1542
1543
  rateLimit: optional({
1543
1544
  max: field(z.number().default(100), { env: "FIRST_TREE_HUB_RATE_LIMIT_MAX" }),
1544
1545
  loginMax: field(z.number().default(5), { env: "FIRST_TREE_HUB_RATE_LIMIT_LOGIN_MAX" }),
1545
- webhookMax: field(z.number().default(60), { env: "FIRST_TREE_HUB_RATE_LIMIT_WEBHOOK_MAX" })
1546
+ webhookMax: field(z.number().default(60), { env: "FIRST_TREE_HUB_RATE_LIMIT_WEBHOOK_MAX" }),
1547
+ agentMessageMax: field(z.number().default(30), { env: "FIRST_TREE_HUB_RATE_LIMIT_AGENT_MESSAGE_MAX" })
1546
1548
  }),
1549
+ ws: optional({ maxPayload: field(z.number().int().min(1024).default(262144), { env: "FIRST_TREE_HUB_WS_MAX_PAYLOAD" }) }),
1547
1550
  inbox: optional({ maxInFlightPerAgent: field(z.number().int().min(1).max(1024).default(32), { env: "FIRST_TREE_HUB_INBOX_MAX_IN_FLIGHT_PER_AGENT" }) }),
1548
1551
  kael: optional({
1549
1552
  endpoint: field(z.string(), { env: "KAEL_ENDPOINT" }),
@@ -6637,6 +6640,37 @@ function runCapture(program, args, timeoutMs) {
6637
6640
  ]
6638
6641
  });
6639
6642
  if (res.status === 0) return { ok: true };
6643
+ if (res.signal) return {
6644
+ ok: false,
6645
+ stderr: `${program} timed out after ${timeoutMs}ms (signal=${res.signal})`,
6646
+ code: null
6647
+ };
6648
+ return {
6649
+ ok: false,
6650
+ stderr: (res.stderr ?? "").trim(),
6651
+ code: res.status
6652
+ };
6653
+ }
6654
+ /** Same as runCapture but also returns stdout — for queries (loginctl show-user, etc.). */
6655
+ function runCaptureOut(program, args, timeoutMs) {
6656
+ const res = spawnSync(program, args, {
6657
+ encoding: "utf-8",
6658
+ timeout: timeoutMs,
6659
+ stdio: [
6660
+ "ignore",
6661
+ "pipe",
6662
+ "pipe"
6663
+ ]
6664
+ });
6665
+ if (res.status === 0) return {
6666
+ ok: true,
6667
+ stdout: (res.stdout ?? "").trim()
6668
+ };
6669
+ if (res.signal) return {
6670
+ ok: false,
6671
+ stderr: `${program} timed out after ${timeoutMs}ms (signal=${res.signal})`,
6672
+ code: null
6673
+ };
6640
6674
  return {
6641
6675
  ok: false,
6642
6676
  stderr: (res.stderr ?? "").trim(),
@@ -6647,8 +6681,40 @@ function sleepSync(ms) {
6647
6681
  const shared = new Int32Array(new SharedArrayBuffer(4));
6648
6682
  Atomics.wait(shared, 0, 0, ms);
6649
6683
  }
6650
- const LAUNCHD_LABEL = "dev.first-tree-hub.client";
6651
- const SYSTEMD_UNIT = "first-tree-hub-client.service";
6684
+ /**
6685
+ * Map a `FIRST_TREE_HUB_HOME` basename to the suffix appended to the
6686
+ * service manager's unit name / label.
6687
+ *
6688
+ * Why this exists: `FIRST_TREE_HUB_HOME` already isolates config /
6689
+ * credentials / workspace under a separate home dir, but until now the
6690
+ * systemd unit name and launchd label were hard-coded — so a developer
6691
+ * running with an isolated home would still rewrite the same
6692
+ * `first-tree-hub-client.service` unit file as the prod install. This
6693
+ * derivation closes that loop: dev homes get their own unit name and
6694
+ * coexist with prod.
6695
+ *
6696
+ * Rule:
6697
+ * - "hub" → "" (default home; preserves the existing prod
6698
+ * unit name `first-tree-hub-client.service` for
6699
+ * every machine already in the field)
6700
+ * - "hub-<x>" → "<x>" ("hub-test" → "test", giving
6701
+ * `first-tree-hub-client-test.service`)
6702
+ * - anything else → the basename verbatim (a custom home like
6703
+ * "~/.first-tree/foo" yields suffix "foo")
6704
+ *
6705
+ * Empty / falsy basenames defensively fall back to the default — we
6706
+ * never want to silently drop a user's intent into prod's unit name.
6707
+ */
6708
+ function deriveServiceSuffix(homeBasename) {
6709
+ if (!homeBasename) return "";
6710
+ if (homeBasename === "hub") return "";
6711
+ if (homeBasename.startsWith("hub-")) return homeBasename.slice(4) || homeBasename;
6712
+ return homeBasename;
6713
+ }
6714
+ const SERVICE_SUFFIX = deriveServiceSuffix(basename(DEFAULT_HOME_DIR$1));
6715
+ const LAUNCHD_LABEL = SERVICE_SUFFIX ? `dev.first-tree-hub.client.${SERVICE_SUFFIX}` : "dev.first-tree-hub.client";
6716
+ const SYSTEMD_UNIT = SERVICE_SUFFIX ? `first-tree-hub-client-${SERVICE_SUFFIX}.service` : "first-tree-hub-client.service";
6717
+ const SYSLOG_IDENT = SERVICE_SUFFIX ? `first-tree-hub-client-${SERVICE_SUFFIX}` : "first-tree-hub-client";
6652
6718
  const LOG_DIR = join(DEFAULT_HOME_DIR$1, "logs");
6653
6719
  function whichBin(name) {
6654
6720
  try {
@@ -6663,23 +6729,36 @@ function whichBin(name) {
6663
6729
  /**
6664
6730
  * Resolve how the service should launch the CLI.
6665
6731
  *
6666
- * Prefers the installed `first-tree-hub` bin on PATH (usually a shim under
6667
- * /usr/local/bin or ~/.npm-global/bin). Falls back to invoking the current
6668
- * Node interpreter against the running script (handles `pnpm dev`, tsx, and
6669
- * dev-only global installs).
6732
+ * Two regimes:
6733
+ *
6734
+ * Prod (default home, empty service suffix) prefer the installed
6735
+ * `first-tree-hub` bin on PATH (usually a shim under /usr/local/bin
6736
+ * or ~/.npm-global/bin). Using the shim means an `npm i -g … @latest`
6737
+ * atomically swaps the binary the unit launches, no unit rewrite
6738
+ * needed.
6739
+ *
6740
+ * ② Dev / isolated (non-empty suffix from a custom FIRST_TREE_HUB_HOME)
6741
+ * — pin to the running interpreter + script path. This skips the
6742
+ * PATH lookup, which would otherwise resolve `first-tree-hub` to
6743
+ * the operator's prod global install — making the dev unit silently
6744
+ * run prod code against a dev home (i.e., the whole isolation story
6745
+ * collapses with no error message). Pinning execPath+argv[1] forces
6746
+ * the dev unit to launch the dev build that just installed it.
6670
6747
  */
6671
- function resolveCliInvocation() {
6672
- const bin = whichBin("first-tree-hub");
6673
- if (bin && isAbsolute(bin)) try {
6674
- return {
6675
- kind: "bin",
6676
- program: realpathSync(bin)
6677
- };
6678
- } catch {
6679
- return {
6680
- kind: "bin",
6681
- program: bin
6682
- };
6748
+ function resolveCliInvocation(serviceSuffix = SERVICE_SUFFIX) {
6749
+ if (serviceSuffix === "") {
6750
+ const bin = whichBin("first-tree-hub");
6751
+ if (bin && isAbsolute(bin)) try {
6752
+ return {
6753
+ kind: "bin",
6754
+ program: realpathSync(bin)
6755
+ };
6756
+ } catch {
6757
+ return {
6758
+ kind: "bin",
6759
+ program: bin
6760
+ };
6761
+ }
6683
6762
  }
6684
6763
  const script = process.argv[1];
6685
6764
  if (!script) throw new Error("Cannot resolve CLI entry point (process.argv[1] is empty).");
@@ -6729,7 +6808,7 @@ ${argsXml}
6729
6808
  <key>PATH</key>
6730
6809
  <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
6731
6810
  <key>FIRST_TREE_HUB_SERVICE_MODE</key>
6732
- <string>1</string>
6811
+ <string>1</string>${SERVICE_SUFFIX ? `\n <key>FIRST_TREE_HUB_HOME</key>\n <string>${escapeXml(DEFAULT_HOME_DIR$1)}</string>` : ""}
6733
6812
  </dict>
6734
6813
  <key>RunAtLoad</key>
6735
6814
  <true/>
@@ -6773,9 +6852,12 @@ function launchdState() {
6773
6852
  const stateLine = out.split(/\r?\n/).find((l) => l.trim().startsWith("state ="));
6774
6853
  const pidLine = out.split(/\r?\n/).find((l) => l.trim().startsWith("pid ="));
6775
6854
  if (stateLine?.includes("running")) {
6776
- const pid = pidLine?.split("=")[1]?.trim();
6855
+ const pidStr = pidLine?.split("=")[1]?.trim();
6856
+ const pidNum = pidStr ? Number(pidStr) : NaN;
6857
+ const pid = Number.isFinite(pidNum) && pidNum > 0 ? pidNum : void 0;
6777
6858
  return {
6778
6859
  state: "active",
6860
+ pid,
6779
6861
  detail: pid ? `pid ${pid}` : "running"
6780
6862
  };
6781
6863
  }
@@ -6818,7 +6900,7 @@ function installLaunchd() {
6818
6900
  if (!bootoutRes.ok) {
6819
6901
  if (!/not find|no such|not loaded/i.test(bootoutRes.stderr)) print.line(` warning: launchctl bootout: ${bootoutRes.stderr || `exit ${bootoutRes.code ?? "unknown"}`}\n`);
6820
6902
  }
6821
- waitForLabelEvicted(target, LAUNCHD_LABEL, 1e4);
6903
+ if (!waitForLabelEvicted(target, LAUNCHD_LABEL, 1e4)) print.line(" warning: launchctl bootout still settling after 10s; bootstrap may need a retry\n");
6822
6904
  let lastBootstrapErr = null;
6823
6905
  for (let attempt = 1; attempt <= 2; attempt++) {
6824
6906
  const res = runCapture("launchctl", [
@@ -6836,13 +6918,14 @@ function installLaunchd() {
6836
6918
  if (lastBootstrapErr) throw new Error(`launchctl bootstrap failed: ${lastBootstrapErr.stderr || `exit ${lastBootstrapErr.code ?? "unknown"}`}\n Command: launchctl bootstrap ${target} ${plistPath}\n Recovery: \`launchctl bootout ${target}/${LAUNCHD_LABEL}\` then \`first-tree-hub client connect <server-url>\`.`);
6837
6919
  const enableRes = runCapture("launchctl", ["enable", `${target}/${LAUNCHD_LABEL}`], 5e3);
6838
6920
  if (!enableRes.ok) print.line(` warning: launchctl enable: ${enableRes.stderr || `exit ${enableRes.code ?? "unknown"}`}\n`);
6839
- const { state, detail } = launchdState();
6921
+ const { state, pid, detail } = launchdState();
6840
6922
  return {
6841
6923
  platform: "launchd",
6842
6924
  label: LAUNCHD_LABEL,
6843
6925
  unitPath: plistPath,
6844
6926
  logDir: LOG_DIR,
6845
6927
  state,
6928
+ pid,
6846
6929
  detail
6847
6930
  };
6848
6931
  }
@@ -6865,20 +6948,25 @@ function systemdUnitPath() {
6865
6948
  function renderSystemdUnit(invocation) {
6866
6949
  return `[Unit]
6867
6950
  Description=First Tree Hub Client
6868
- After=network-online.target
6869
- Wants=network-online.target
6951
+ StartLimitIntervalSec=300
6952
+ StartLimitBurst=10
6870
6953
 
6871
6954
  [Service]
6872
6955
  Type=simple
6873
6956
  ExecStart=${invocation.kind === "bin" ? `${shellQuote(invocation.program)} client start --no-interactive` : `${shellQuote(invocation.program)} ${invocation.args.map(shellQuote).join(" ")} client start --no-interactive`}
6874
- Restart=always
6957
+ Restart=on-failure
6875
6958
  RestartSec=10
6876
- StandardOutput=append:${join(LOG_DIR, "client.stdout.log")}
6877
- StandardError=append:${join(LOG_DIR, "client.stderr.log")}
6959
+ SuccessExitStatus=0
6960
+ RestartForceExitStatus=75
6961
+ KillSignal=SIGTERM
6962
+ KillMode=mixed
6963
+ TimeoutStopSec=30
6964
+ StandardOutput=journal
6965
+ StandardError=journal
6966
+ SyslogIdentifier=${SYSLOG_IDENT}
6878
6967
  Environment=PATH=/usr/local/bin:/usr/bin:/bin
6879
6968
  Environment=FIRST_TREE_HUB_SERVICE_MODE=1
6880
-
6881
- [Install]
6969
+ ${SERVICE_SUFFIX ? `Environment=FIRST_TREE_HUB_HOME=${shellQuote(DEFAULT_HOME_DIR$1)}\n` : ""}[Install]
6882
6970
  WantedBy=default.target
6883
6971
  `;
6884
6972
  }
@@ -6902,15 +6990,75 @@ function systemdState() {
6902
6990
  ]
6903
6991
  });
6904
6992
  const out = (res.stdout ?? "").trim();
6905
- if (res.status === 0 && out === "active") return {
6906
- state: "active",
6907
- detail: "running"
6908
- };
6993
+ if (res.status === 0 && out === "active") {
6994
+ const pid = readSystemdMainPid();
6995
+ return {
6996
+ state: "active",
6997
+ pid,
6998
+ detail: pid ? `pid ${pid}` : "running"
6999
+ };
7000
+ }
6909
7001
  return {
6910
7002
  state: "inactive",
6911
7003
  detail: out || "unit present but not active"
6912
7004
  };
6913
7005
  }
7006
+ function readSystemdMainPid() {
7007
+ const res = runCaptureOut("systemctl", [
7008
+ "--user",
7009
+ "show",
7010
+ SYSTEMD_UNIT,
7011
+ "-p",
7012
+ "MainPID",
7013
+ "--value"
7014
+ ], 5e3);
7015
+ if (!res.ok) return void 0;
7016
+ const n = Number(res.stdout);
7017
+ return Number.isFinite(n) && n > 0 ? n : void 0;
7018
+ }
7019
+ /**
7020
+ * Best-effort `loginctl enable-linger` for the current user.
7021
+ *
7022
+ * Why this matters: a `--user` systemd service is tied to the user's session.
7023
+ * Without linger, when the user logs out (closes their last SSH session,
7024
+ * graphical session ends, etc.) the user's systemd manager exits and stops
7025
+ * every service it owns — including ours. The next login restarts everything,
7026
+ * which is silently wrong: agents go offline for hours and the operator has
7027
+ * no obvious cause.
7028
+ *
7029
+ * `enable-linger <self>` is allowed without sudo on systemd ≥ 240 thanks to
7030
+ * polkit's `org.freedesktop.login1.set-self-linger` rule. On older distros
7031
+ * or hardened setups it requires polkit auth — we don't try to escalate;
7032
+ * the warning printed by the caller is the operator's signal to run it
7033
+ * manually.
7034
+ */
7035
+ function tryEnableLinger() {
7036
+ const username = userInfo().username;
7037
+ if (!username) return {
7038
+ ok: false,
7039
+ reason: "could not determine username"
7040
+ };
7041
+ const showRes = runCaptureOut("loginctl", [
7042
+ "show-user",
7043
+ username,
7044
+ "-p",
7045
+ "Linger",
7046
+ "--value"
7047
+ ], 5e3);
7048
+ if (showRes.ok && showRes.stdout === "yes") return {
7049
+ ok: true,
7050
+ alreadyOn: true
7051
+ };
7052
+ const res = runCapture("loginctl", ["enable-linger", username], 5e3);
7053
+ if (res.ok) return {
7054
+ ok: true,
7055
+ alreadyOn: false
7056
+ };
7057
+ return {
7058
+ ok: false,
7059
+ reason: res.stderr || `exit ${res.code ?? "unknown"}`
7060
+ };
7061
+ }
6914
7062
  function installSystemd() {
6915
7063
  const invocation = resolveCliInvocation();
6916
7064
  ensureLogDir();
@@ -6919,6 +7067,8 @@ function installSystemd() {
6919
7067
  writeFileSync(unitPath, renderSystemdUnit(invocation), { mode: 420 });
6920
7068
  const reloadRes = runCapture("systemctl", ["--user", "daemon-reload"], 5e3);
6921
7069
  if (!reloadRes.ok) throw new Error(`systemctl --user daemon-reload failed: ${reloadRes.stderr || `exit ${reloadRes.code ?? "unknown"}`}`);
7070
+ const lingerRes = tryEnableLinger();
7071
+ if (!lingerRes.ok) print.line(` warning: loginctl enable-linger failed: ${lingerRes.reason}\n The service will stop when you log out. Run manually: sudo loginctl enable-linger ${userInfo().username}\n`);
6922
7072
  const enableRes = runCapture("systemctl", [
6923
7073
  "--user",
6924
7074
  "enable",
@@ -6926,13 +7076,14 @@ function installSystemd() {
6926
7076
  SYSTEMD_UNIT
6927
7077
  ], 1e4);
6928
7078
  if (!enableRes.ok) throw new Error(`systemctl --user enable --now ${SYSTEMD_UNIT} failed: ${enableRes.stderr || `exit ${enableRes.code ?? "unknown"}`}\n Recovery: \`systemctl --user stop ${SYSTEMD_UNIT}\` then \`first-tree-hub client connect <server-url>\`.`);
6929
- const { state, detail } = systemdState();
7079
+ const { state, pid, detail } = systemdState();
6930
7080
  return {
6931
7081
  platform: "systemd",
6932
7082
  label: SYSTEMD_UNIT,
6933
7083
  unitPath,
6934
7084
  logDir: LOG_DIR,
6935
7085
  state,
7086
+ pid,
6936
7087
  detail
6937
7088
  };
6938
7089
  }
@@ -6973,24 +7124,26 @@ function installClientService() {
6973
7124
  /** Report the current service state without modifying anything. */
6974
7125
  function getClientServiceStatus() {
6975
7126
  if (process.platform === "darwin") {
6976
- const { state, detail } = launchdState();
7127
+ const { state, pid, detail } = launchdState();
6977
7128
  return {
6978
7129
  platform: "launchd",
6979
7130
  label: LAUNCHD_LABEL,
6980
7131
  unitPath: launchdPlistPath(),
6981
7132
  logDir: LOG_DIR,
6982
7133
  state,
7134
+ pid,
6983
7135
  detail
6984
7136
  };
6985
7137
  }
6986
7138
  if (process.platform === "linux") {
6987
- const { state, detail } = systemdState();
7139
+ const { state, pid, detail } = systemdState();
6988
7140
  return {
6989
7141
  platform: "systemd",
6990
7142
  label: SYSTEMD_UNIT,
6991
7143
  unitPath: systemdUnitPath(),
6992
7144
  logDir: LOG_DIR,
6993
7145
  state,
7146
+ pid,
6994
7147
  detail
6995
7148
  };
6996
7149
  }
@@ -7003,6 +7156,137 @@ function getClientServiceStatus() {
7003
7156
  detail: `platform ${process.platform} not supported`
7004
7157
  };
7005
7158
  }
7159
+ /** Start the service. No-op + ok if already running. */
7160
+ function startClientService() {
7161
+ if (process.platform === "linux") {
7162
+ const res = runCapture("systemctl", [
7163
+ "--user",
7164
+ "start",
7165
+ SYSTEMD_UNIT
7166
+ ], 15e3);
7167
+ if (!res.ok) return {
7168
+ ok: false,
7169
+ reason: res.stderr || `exit ${res.code ?? "unknown"}`
7170
+ };
7171
+ return { ok: true };
7172
+ }
7173
+ if (process.platform === "darwin") {
7174
+ const target = launchctlDomainTarget();
7175
+ const plistPath = launchdPlistPath();
7176
+ if (!existsSync(plistPath)) return {
7177
+ ok: false,
7178
+ reason: "service not installed"
7179
+ };
7180
+ if (runCaptureOut("launchctl", ["print", `${target}/${LAUNCHD_LABEL}`], 5e3).ok) {
7181
+ const res = runCapture("launchctl", ["kickstart", `${target}/${LAUNCHD_LABEL}`], 1e4);
7182
+ if (!res.ok) return {
7183
+ ok: false,
7184
+ reason: res.stderr || `exit ${res.code ?? "unknown"}`
7185
+ };
7186
+ return { ok: true };
7187
+ }
7188
+ const res = runCapture("launchctl", [
7189
+ "bootstrap",
7190
+ target,
7191
+ plistPath
7192
+ ], 1e4);
7193
+ if (!res.ok) return {
7194
+ ok: false,
7195
+ reason: res.stderr || `exit ${res.code ?? "unknown"}`
7196
+ };
7197
+ return { ok: true };
7198
+ }
7199
+ return {
7200
+ ok: false,
7201
+ reason: `service control not supported on ${process.platform}`
7202
+ };
7203
+ }
7204
+ /**
7205
+ * Stop the service without disabling auto-start on next boot/login.
7206
+ *
7207
+ * systemd: `systemctl --user stop` — unit stays enabled, so a reboot or
7208
+ * `client start` brings it back. Combined with `Restart=on-failure +
7209
+ * SuccessExitStatus=0` in the unit, the SIGTERM path actually terminates
7210
+ * (the bug `Restart=always` had: stop would be immediately undone).
7211
+ *
7212
+ * launchd: `launchctl bootout` — unloads the running registration but
7213
+ * leaves the plist in `~/Library/LaunchAgents/`, so the next user login
7214
+ * (or `client start`) reloads it.
7215
+ */
7216
+ function stopClientService() {
7217
+ if (process.platform === "linux") {
7218
+ const res = runCapture("systemctl", [
7219
+ "--user",
7220
+ "stop",
7221
+ SYSTEMD_UNIT
7222
+ ], 35e3);
7223
+ if (!res.ok) return {
7224
+ ok: false,
7225
+ reason: res.stderr || `exit ${res.code ?? "unknown"}`
7226
+ };
7227
+ return { ok: true };
7228
+ }
7229
+ if (process.platform === "darwin") {
7230
+ const res = runCapture("launchctl", ["bootout", `${launchctlDomainTarget()}/${LAUNCHD_LABEL}`], 3e4);
7231
+ if (!res.ok) {
7232
+ if (/not find|no such|not loaded/i.test(res.stderr)) return {
7233
+ ok: true,
7234
+ detail: "not running"
7235
+ };
7236
+ return {
7237
+ ok: false,
7238
+ reason: res.stderr || `exit ${res.code ?? "unknown"}`
7239
+ };
7240
+ }
7241
+ return { ok: true };
7242
+ }
7243
+ return {
7244
+ ok: false,
7245
+ reason: `service control not supported on ${process.platform}`
7246
+ };
7247
+ }
7248
+ /** Restart the service. Equivalent to stop + start, but uses the manager's atomic primitive. */
7249
+ function restartClientService() {
7250
+ if (process.platform === "linux") {
7251
+ const res = runCapture("systemctl", [
7252
+ "--user",
7253
+ "restart",
7254
+ SYSTEMD_UNIT
7255
+ ], 45e3);
7256
+ if (!res.ok) return {
7257
+ ok: false,
7258
+ reason: res.stderr || `exit ${res.code ?? "unknown"}`
7259
+ };
7260
+ return { ok: true };
7261
+ }
7262
+ if (process.platform === "darwin") {
7263
+ const target = launchctlDomainTarget();
7264
+ const plistPath = launchdPlistPath();
7265
+ if (!existsSync(plistPath)) return {
7266
+ ok: false,
7267
+ reason: "service not installed"
7268
+ };
7269
+ if (runCapture("launchctl", [
7270
+ "kickstart",
7271
+ "-k",
7272
+ `${target}/${LAUNCHD_LABEL}`
7273
+ ], 3e4).ok) return { ok: true };
7274
+ const bootstrapRes = runCapture("launchctl", [
7275
+ "bootstrap",
7276
+ target,
7277
+ plistPath
7278
+ ], 1e4);
7279
+ if (!bootstrapRes.ok) return {
7280
+ ok: false,
7281
+ reason: bootstrapRes.stderr || `exit ${bootstrapRes.code ?? "unknown"}`
7282
+ };
7283
+ return { ok: true };
7284
+ }
7285
+ return {
7286
+ ok: false,
7287
+ reason: `service control not supported on ${process.platform}`
7288
+ };
7289
+ }
7006
7290
  /** Uninstall the background service. No-op if not installed. */
7007
7291
  function uninstallClientService() {
7008
7292
  if (process.platform === "darwin") return uninstallLaunchd();
@@ -8756,7 +9040,7 @@ function createFeedbackHandler(config) {
8756
9040
  return { handle };
8757
9041
  }
8758
9042
  //#endregion
8759
- //#region ../server/dist/app-B1rxbSHG.mjs
9043
+ //#region ../server/dist/app-O9kCTpaF.mjs
8760
9044
  var __defProp = Object.defineProperty;
8761
9045
  var __exportAll = (all, no_symbols) => {
8762
9046
  let target = {};
@@ -14195,8 +14479,40 @@ const editMessageSchema = z.object({
14195
14479
  format: z.string().optional(),
14196
14480
  content: z.unknown()
14197
14481
  });
14482
+ /**
14483
+ * Per-agent rate limit on outbound message writes. Keyed by `agent.uuid`
14484
+ * (populated by `agentSelectorHook`, which runs as an onRequest hook before
14485
+ * the global limiter — registered with `hook: "preHandler"` — fires).
14486
+ *
14487
+ * Rationale: agent ↔ agent reply loops are the documented failure mode
14488
+ * (`mention_only` is the semantic guard; this is the hard ceiling).
14489
+ *
14490
+ * The IP fallback is **defensive scaffolding, not a real code path**. These
14491
+ * routes mount under `/agent` which forces `memberAuth + agentSelector`
14492
+ * onRequest hooks (see app.ts) — a missing `req.agent` would have already
14493
+ * 403'd before this preHandler runs. The fallback exists so that if a future
14494
+ * refactor reorders hooks (or detaches one of these routes from the agent
14495
+ * scope), the limiter degrades to per-IP keying with a logged warning rather
14496
+ * than silently keying everyone to the same `undefined` bucket.
14497
+ */
14498
+ function agentMessageWriteRateLimit(max) {
14499
+ return { rateLimit: {
14500
+ max,
14501
+ timeWindow: "1 minute",
14502
+ keyGenerator: (req) => {
14503
+ const agentId = req.agent?.uuid;
14504
+ if (agentId) return `agent:${agentId}`;
14505
+ log$2.warn({
14506
+ ip: req.ip,
14507
+ route: req.routeOptions?.url ?? req.url
14508
+ }, "rate-limit keyGenerator fell back to IP — req.agent missing on a route under /agent (hook order regression?)");
14509
+ return `ip:${req.ip}`;
14510
+ }
14511
+ } };
14512
+ }
14198
14513
  async function agentMessageRoutes(app) {
14199
- app.post("/:chatId/messages", async (request, reply) => {
14514
+ const writeRateLimit = agentMessageWriteRateLimit(app.config.rateLimit?.agentMessageMax ?? 30);
14515
+ app.post("/:chatId/messages", { config: writeRateLimit }, async (request, reply) => {
14200
14516
  const identity = requireAgent(request);
14201
14517
  await assertParticipant(app.db, request.params.chatId, identity.uuid);
14202
14518
  const body = sendMessageSchema.parse(request.body);
@@ -14240,7 +14556,8 @@ async function agentMessageRoutes(app) {
14240
14556
  });
14241
14557
  }
14242
14558
  async function agentSendToAgentRoutes(app) {
14243
- app.post("/:name/messages", async (request, reply) => {
14559
+ const writeRateLimit = agentMessageWriteRateLimit(app.config.rateLimit?.agentMessageMax ?? 30);
14560
+ app.post("/:name/messages", { config: writeRateLimit }, async (request, reply) => {
14244
14561
  const identity = requireAgent(request);
14245
14562
  const body = sendToAgentSchema.parse(request.body);
14246
14563
  const { message: msg, recipients } = await sendToAgent(app.db, identity.uuid, request.params.name, body);
@@ -18015,7 +18332,11 @@ async function buildApp(config) {
18015
18332
  format: config.observability.logging.format,
18016
18333
  bridgeToSpanLevel: config.observability.logging.bridgeToSpanLevel
18017
18334
  });
18018
- const app = Fastify({ loggerInstance: rootLogger$1 });
18335
+ const app = Fastify({
18336
+ loggerInstance: rootLogger$1,
18337
+ trustProxy: config.trustProxy
18338
+ });
18339
+ if (config.trustProxy) app.log.warn("trustProxy=true — Fastify trusts ANY upstream's x-forwarded-for. Ensure Cloudflare / CapRover is the only ingress; do NOT expose this container's port to the public internet directly.");
18019
18340
  const otelPlugin = getFastifyOtelPlugin();
18020
18341
  if (otelPlugin) await app.register(otelPlugin);
18021
18342
  await app.register(observabilityPlugin);
@@ -18027,7 +18348,7 @@ async function buildApp(config) {
18027
18348
  app.log.info({ commandVersion }, "Hub server advertising command version");
18028
18349
  const listenClient = postgres(config.database.url, { max: 1 });
18029
18350
  const notifier = createNotifier(listenClient);
18030
- await app.register(websocket);
18351
+ await app.register(websocket, { options: { maxPayload: config.ws?.maxPayload ?? 65536 } });
18031
18352
  const corsOrigin = config.cors?.origin;
18032
18353
  const isDev = process.env.NODE_ENV !== "production";
18033
18354
  await app.register(cors, {
@@ -18036,7 +18357,8 @@ async function buildApp(config) {
18036
18357
  });
18037
18358
  await app.register(rateLimit, {
18038
18359
  max: config.rateLimit?.max ?? 100,
18039
- timeWindow: "1 minute"
18360
+ timeWindow: "1 minute",
18361
+ hook: "preHandler"
18040
18362
  });
18041
18363
  const memberAuth = memberAuthHook(db, config.secrets.jwtSecret);
18042
18364
  const adminOnly = requireAdminRoleHook();
@@ -18053,6 +18375,10 @@ async function buildApp(config) {
18053
18375
  details: error.issues,
18054
18376
  ...traceField
18055
18377
  });
18378
+ if (error instanceof Error && "statusCode" in error && typeof error.statusCode === "number" && error.statusCode >= 400 && error.statusCode < 500) return reply.status(error.statusCode).send({
18379
+ error: error.message,
18380
+ ...traceField
18381
+ });
18056
18382
  request.log.error({ err: error }, "unhandled request error");
18057
18383
  return reply.status(500).send({
18058
18384
  error: "Internal server error",
@@ -18426,12 +18752,21 @@ function resolveNpmCommand() {
18426
18752
  */
18427
18753
  function detectInstallMode(argv1 = process.argv[1] ?? "") {
18428
18754
  if (!argv1) return "npx";
18429
- let dir = dirname(resolve(argv1));
18755
+ const start = dirname(resolve(argv1));
18756
+ {
18757
+ let dir = start;
18758
+ for (let i = 0; i < 10; i++) {
18759
+ if (existsSync(resolve(dir, ".git"))) return "source";
18760
+ const parent = dirname(dir);
18761
+ if (parent === dir) break;
18762
+ dir = parent;
18763
+ }
18764
+ }
18765
+ let dir = start;
18430
18766
  for (let i = 0; i < 10; i++) {
18431
- if (existsSync(resolve(dir, ".git"))) return "source";
18432
18767
  const pkgPath = resolve(dir, "package.json");
18433
18768
  if (existsSync(pkgPath)) try {
18434
- if (JSON.parse(readFileSync(pkgPath, "utf8")).name === PACKAGE_NAME) {
18769
+ if (JSON.parse(readFileSync(pkgPath, "utf8")).name === "@agent-team-foundation/first-tree-hub") {
18435
18770
  if (/\/(?:_npx|\.npm\/_npx)\//.test(dir)) return "npx";
18436
18771
  return "global";
18437
18772
  }
@@ -18505,6 +18840,42 @@ function parseInstalledVersion(stdout) {
18505
18840
  function escapeForRegex(s) {
18506
18841
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18507
18842
  }
18843
+ /**
18844
+ * Look up the latest published version of the CLI package.
18845
+ *
18846
+ * Uses `npm view <pkg> version` (rather than fetch'ing registry.npmjs.org
18847
+ * directly) so the user's `.npmrc` registry, proxy, and auth settings are
18848
+ * honored — important for corporate users routed through Verdaccio /
18849
+ * Artifactory mirrors.
18850
+ */
18851
+ function fetchLatestVersion(timeoutMs = 1e4) {
18852
+ const res = spawnSync(resolveNpmCommand(), [
18853
+ "view",
18854
+ PACKAGE_NAME,
18855
+ "version"
18856
+ ], {
18857
+ encoding: "utf-8",
18858
+ timeout: timeoutMs,
18859
+ stdio: [
18860
+ "ignore",
18861
+ "pipe",
18862
+ "pipe"
18863
+ ]
18864
+ });
18865
+ if (res.status !== 0) return {
18866
+ ok: false,
18867
+ reason: (res.stderr ?? "").trim() || `npm view exited with code ${res.status}`
18868
+ };
18869
+ const version = (res.stdout ?? "").trim();
18870
+ if (!semver.valid(version)) return {
18871
+ ok: false,
18872
+ reason: `npm view returned non-semver value: ${version.slice(0, 80)}`
18873
+ };
18874
+ return {
18875
+ ok: true,
18876
+ version
18877
+ };
18878
+ }
18508
18879
  /** Interactive update prompt. Defaults to N on timeout. */
18509
18880
  const promptUpdate = async ({ currentVersion, targetVersion, timeoutSeconds }) => {
18510
18881
  const message = `A newer First Tree Hub client is available.\n You: ${currentVersion}\n Server bundled with: ${targetVersion}\n Will install: latest on npm (>= ${targetVersion})\n Updating will restart the client and briefly interrupt any active sessions.\n Update now?`;
@@ -18754,4 +19125,4 @@ function registerSaaSConnectCommand(program) {
18754
19125
  });
18755
19126
  }
18756
19127
  //#endregion
18757
- export { success as $, checkServerHealth as A, ensurePostgres as B, checkAgentConfigs as C, checkDocker as D, checkDatabase as E, getClientServiceStatus as F, rotateClientIdWithBackup as G, stopPostgres as H, installClientService as I, removeLocalAgent as J, findStaleAliases as K, isServiceSupported as L, checkWebSocket as M, printResults as N, checkNodeVersion as O, reconcileAgentConfigs as P, fail as Q, resolveCliInvocation as R, runMigrations as S, checkClientConfig as T, ClientRuntime as U, isDockerAvailable as V, handleClientOrgMismatch as W, hasUser as X, createOwner as Y, resolveReplyToFromEnv as Z, onboardCreate as _, declineUpdate as a, ClientUserMismatchError as at, createApiNameResolver as b, COMMAND_VERSION as c, SessionRegistry as ct, isInteractive as d, applyClientLoggerConfig as dt, blank as et, promptAddAgent as f, configureClientLoggerForService as ft, onboardCheck as g, loadOnboardState as h, createExecuteUpdate as i, ClientOrgMismatchError as it, checkServerReachable as j, checkServerConfig as k, reconcileLocalRuntimeProviders as l, cleanWorkspaces as lt, formatCheckReport as m, deriveHubUrlFromToken as n, setJsonMode as nt, promptUpdate as o, FirstTreeHubSDK as ot, promptMissingFields as p, formatStaleReason as q, registerSaaSConnectCommand as r, status as rt, startServer as s, SdkError as st, HubUrlDerivationError as t, print as tt, uploadClientCapabilities as u, probeCapabilities as ut, saveOnboardState as v, checkBackgroundService as w, migrateLocalAgentDirs as x, runHomeMigration as y, uninstallClientService as z };
19128
+ export { findStaleAliases as $, checkDatabase as A, installClientService as B, runHomeMigration as C, checkAgentConfigs as D, runMigrations as E, checkServerReachable as F, stopClientService as G, resolveCliInvocation as H, checkWebSocket as I, isDockerAvailable as J, uninstallClientService as K, printResults as L, checkNodeVersion as M, checkServerConfig as N, checkBackgroundService as O, checkServerHealth as P, rotateClientIdWithBackup as Q, reconcileAgentConfigs as R, saveOnboardState as S, migrateLocalAgentDirs as T, restartClientService as U, isServiceSupported as V, startClientService as W, ClientRuntime as X, stopPostgres as Y, handleClientOrgMismatch as Z, promptMissingFields as _, probeCapabilities as _t, declineUpdate as a, fail as at, onboardCheck as b, detectInstallMode as c, print as ct, startServer as d, ClientOrgMismatchError as dt, formatStaleReason as et, COMMAND_VERSION as f, ClientUserMismatchError as ft, promptAddAgent as g, cleanWorkspaces as gt, isInteractive as h, SessionRegistry as ht, createExecuteUpdate as i, resolveReplyToFromEnv as it, checkDocker as j, checkClientConfig as k, fetchLatestVersion as l, setJsonMode as lt, uploadClientCapabilities as m, SdkError as mt, deriveHubUrlFromToken as n, createOwner as nt, promptUpdate as o, success as ot, reconcileLocalRuntimeProviders as p, FirstTreeHubSDK as pt, ensurePostgres as q, registerSaaSConnectCommand as r, hasUser as rt, PACKAGE_NAME as s, blank as st, HubUrlDerivationError as t, removeLocalAgent as tt, installGlobalLatest as u, status as ut, formatCheckReport as v, applyClientLoggerConfig as vt, createApiNameResolver as w, onboardCreate as x, loadOnboardState as y, configureClientLoggerForService as yt, getClientServiceStatus as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-team-foundation/first-tree-hub",
3
- "version": "0.10.10",
3
+ "version": "0.10.11",
4
4
  "type": "module",
5
5
  "description": "First Tree Hub — unified CLI for server, client, and agent management",
6
6
  "exports": {