@agent-team-foundation/first-tree-hub 0.7.0 → 0.7.2

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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { C as setConfigValue, S as serverConfigSchema, _ as loadAgents, b as resetConfigMeta, c as saveCredentials, f as agentConfigSchema, g as initConfig, h as getConfigValue, 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, v as readConfigFile, y as resetConfig } from "../bootstrap-CRDR6NwE.mjs";
3
- import { A as SdkError, D as createOwner, E as ClientRuntime, M as cleanWorkspaces, T as stopPostgres, _ as checkServerHealth, a as formatCheckReport, b as printResults, c as onboardCreate, d as checkAgentConfigs, f as checkClientConfig, g as checkServerConfig, h as checkNodeVersion, i as promptMissingFields, j as SessionRegistry, k as FirstTreeHubSDK, l as saveOnboardState, m as checkDocker, n as isInteractive, o as loadOnboardState, p as checkDatabase, r as promptAddAgent, s as onboardCheck, t as startServer, u as runMigrations, v as checkServerReachable, y as checkWebSocket } from "../core-4nvleGlC.mjs";
3
+ import { A as stopPostgres, C as checkServerReachable, F as SdkError, I as SessionRegistry, L as cleanWorkspaces, M as createOwner, P as FirstTreeHubSDK, S as checkServerHealth, T as printResults, _ as checkClientConfig, a as uninstallClientService, b as checkNodeVersion, c as promptAddAgent, d as loadOnboardState, f as onboardCheck, g as checkAgentConfigs, h as runMigrations, j as ClientRuntime, l as promptMissingFields, m as saveOnboardState, n as installClientService, o as startServer, p as onboardCreate, r as isServiceSupported, s as isInteractive, t as getClientServiceStatus, u as formatCheckReport, v as checkDatabase, w as checkWebSocket, x as checkServerConfig, y as checkDocker } from "../core-6-paFwyo.mjs";
4
4
  import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-CJ08ntOD.mjs";
5
5
  import { createRequire } from "node:module";
6
6
  import { Command } from "commander";
@@ -798,7 +798,7 @@ async function authenticateInteractive(url) {
798
798
  return await loginRes.json();
799
799
  }
800
800
  function registerConnectCommand(parent) {
801
- parent.command("connect <server-url>").description("Connect to a Hub server — configure, authenticate, and start client").option("--token <token>", "Connect token (from Hub web console) — skips interactive login").action(async (serverUrl, options) => {
801
+ parent.command("connect <server-url>").description("Connect to a Hub server — configure, authenticate, and install the background service").option("--token <token>", "Connect token (from Hub web console) — skips interactive login").option("--no-service", "Skip background service install (runs inline until Ctrl+C)").action(async (serverUrl, options) => {
802
802
  try {
803
803
  const url = serverUrl.replace(/\/+$/, "");
804
804
  setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", url);
@@ -814,12 +814,23 @@ function registerConnectCommand(parent) {
814
814
  schema: clientConfigSchema,
815
815
  role: "client"
816
816
  });
817
+ process.stderr.write(` \u2713 Connected as this computer (id: ${config.client.id})\n`);
818
+ if (options.service !== false && isServiceSupported()) {
819
+ const info = installClientService();
820
+ process.stderr.write(` \u2713 Installed as a background service (${info.platform}) — you can close this terminal\n\n`);
821
+ process.stderr.write(` Unit: ${info.unitPath}\n`);
822
+ process.stderr.write(` Logs: ${info.logDir}\n`);
823
+ if (info.state === "active" && info.detail) process.stderr.write(` State: running (${info.detail})\n`);
824
+ process.stderr.write("\n");
825
+ return;
826
+ }
827
+ if (options.service === false) process.stderr.write(" (--no-service) running inline — Ctrl+C to stop\n");
828
+ else process.stderr.write(` Background service not supported on ${process.platform}; running inline.\n`);
817
829
  const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
818
830
  const agents = loadAgents({
819
831
  schema: agentConfigSchema,
820
832
  agentsDir
821
833
  });
822
- process.stderr.write(`\n Starting client (id: ${config.client.id})...\n`);
823
834
  const runtime = new ClientRuntime(config.server.url, config.client.id);
824
835
  for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
825
836
  await runtime.start();
@@ -923,6 +934,48 @@ function registerClientCommands(program) {
923
934
  process.stderr.write(" No agents directory found.\n");
924
935
  }
925
936
  });
937
+ const service = client.command("service").description("Install/uninstall the background service that keeps this computer online");
938
+ service.command("install").description("Install as a background service — auto-starts on login/boot").action(() => {
939
+ if (!isServiceSupported()) {
940
+ process.stderr.write(` Background service is not supported on ${process.platform}.\n Run \`first-tree-hub client start\` manually to keep the computer online.
941
+ `);
942
+ process.exit(1);
943
+ }
944
+ try {
945
+ const info = installClientService();
946
+ process.stderr.write(`\n \u2713 Installed as a background service (${info.platform}).\n`);
947
+ process.stderr.write(` Unit: ${info.unitPath}\n`);
948
+ process.stderr.write(` Logs: ${info.logDir}\n`);
949
+ if (info.state === "active") process.stderr.write(` State: running${info.detail ? ` (${info.detail})` : ""}\n`);
950
+ else process.stderr.write(` State: ${info.state}${info.detail ? ` (${info.detail})` : ""}\n`);
951
+ process.stderr.write("\n You can close this terminal — the computer stays online.\n");
952
+ } catch (error) {
953
+ fail("SERVICE_INSTALL_ERROR", error instanceof Error ? error.message : String(error));
954
+ }
955
+ });
956
+ service.command("status").description("Show background service state").action(() => {
957
+ const info = getClientServiceStatus();
958
+ if (info.platform === "unsupported") {
959
+ process.stderr.write(` Not supported on ${process.platform}.\n`);
960
+ return;
961
+ }
962
+ process.stderr.write(`\n ${info.platform}: ${info.label}\n`);
963
+ process.stderr.write(` Unit: ${info.unitPath}\n`);
964
+ process.stderr.write(` Logs: ${info.logDir}\n`);
965
+ process.stderr.write(` State: ${info.state}${info.detail ? ` (${info.detail})` : ""}\n\n`);
966
+ });
967
+ service.command("uninstall").description("Stop and remove the background service").action(() => {
968
+ if (!isServiceSupported()) {
969
+ process.stderr.write(` Not supported on ${process.platform}.\n`);
970
+ return;
971
+ }
972
+ try {
973
+ const info = uninstallClientService();
974
+ process.stderr.write(`\n \u2713 Uninstalled background service (${info.platform}).\n\n`);
975
+ } catch (error) {
976
+ fail("SERVICE_UNINSTALL_ERROR", error instanceof Error ? error.message : String(error));
977
+ }
978
+ });
926
979
  client.command("hub-list").description("List clients on the Hub server").option("--server <url>", "Hub server URL").action(async (options) => {
927
980
  try {
928
981
  const serverUrl = resolveServerUrl(options.server);
@@ -1,11 +1,11 @@
1
1
  import { C as setConfigValue, S as serverConfigSchema, _ as loadAgents, 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, x as resolveConfigReadonly } from "./bootstrap-CRDR6NwE.mjs";
2
2
  import { $ as updateAgentSchema, A as createOrganizationSchema, B as paginationQuerySchema, C as clientRegisterSchema, D as createAgentSchema, E as createAdapterMappingSchema, F as isRedactedEnvValue, G as sendToAgentSchema, H as runtimeStateMessageSchema, I as linkTaskChatSchema, J as sessionEventSchema$1, K as sessionCompletionMessageSchema, L as loginSchema, M as delegateFeishuUserSchema, N as dryRunAgentRuntimeConfigSchema, O as createChatSchema, P as inboxPollQuerySchema, Q as updateAgentRuntimeConfigSchema, R as messageSourceSchema$1, S as agentTypeSchema$1, T as createAdapterConfigSchema, U as selfServiceFeishuBotSchema, V as refreshTokenSchema, W as sendMessageSchema, X as taskListQuerySchema, Y as sessionStateMessageSchema, Z as updateAdapterConfigSchema, _ as addParticipantSchema, a as AGENT_SELECTOR_HEADER$1, b as agentBindRequestSchema, c as AGENT_TYPES, d as SYSTEM_CONFIG_DEFAULTS, et as updateMemberSchema, f as TASK_CREATOR_TYPES, g as WS_AUTH_FRAME_TIMEOUT_MS, h as TASK_TERMINAL_STATUSES, i as AGENT_BIND_REJECT_REASONS, it as wsAuthFrameSchema, j as createTaskSchema, k as createMemberSchema, l as AGENT_VISIBILITY, m as TASK_STATUSES, nt as updateSystemConfigSchema, o as AGENT_SOURCES, p as TASK_HEALTH_SIGNALS, q as sessionEventMessageSchema, rt as updateTaskStatusSchema, s as AGENT_STATUSES, tt as updateOrganizationSchema, u as DEFAULT_AGENT_RUNTIME_CONFIG_PAYLOAD, v as adminCreateTaskSchema, w as connectTokenExchangeSchema, x as agentRuntimeConfigPayloadSchema$1, y as adminUpdateTaskSchema, z as notificationQuerySchema } from "./feishu-CJ08ntOD.mjs";
3
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
4
- import { dirname, join, resolve } from "node:path";
3
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
4
+ import { dirname, isAbsolute, join, resolve } from "node:path";
5
5
  import { ZodError, z } from "zod";
6
6
  import "yaml";
7
7
  import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
8
- import { homedir, hostname, platform } from "node:os";
8
+ import { homedir, hostname, platform, userInfo } from "node:os";
9
9
  import { EventEmitter } from "node:events";
10
10
  import WebSocket from "ws";
11
11
  import { query } from "@anthropic-ai/claude-agent-sdk";
@@ -1106,7 +1106,6 @@ var ClientConnection = class extends EventEmitter {
1106
1106
  return;
1107
1107
  }
1108
1108
  if (type === "auth:rejected" || type === "auth:expired") {
1109
- this.registered = false;
1110
1109
  if (type === "auth:expired") this.emit("auth:expired");
1111
1110
  this.ws?.close(4401, type);
1112
1111
  return;
@@ -1494,6 +1493,451 @@ echo "long message body" | first-tree-hub agent send <agentId>
1494
1493
  For content with quotes, \`$\`, backticks, or newlines, prefer stdin to avoid shell escaping issues.
1495
1494
  `;
1496
1495
  }
1496
+ const DEFAULT_CLONE_TIMEOUT_MS = 300 * 1e3;
1497
+ const FETCH_REFSPEC = "+refs/heads/*:refs/remotes/origin/*";
1498
+ const SESSION_BRANCH_PREFIX = "hub-session";
1499
+ function hashUrl(url) {
1500
+ return createHash("sha256").update(url).digest("hex").slice(0, 32);
1501
+ }
1502
+ function shortHash(input) {
1503
+ return createHash("sha256").update(input).digest("hex").slice(0, 8);
1504
+ }
1505
+ function deriveSessionBranchName(sessionKey, url) {
1506
+ return `${SESSION_BRANCH_PREFIX}-${shortHash(sessionKey)}-${shortHash(url)}`;
1507
+ }
1508
+ /**
1509
+ * A value is SHA-like when it's a 7–40 character hex string. Used to decide
1510
+ * whether `ref` should be resolved via the remote namespace (branch name) or
1511
+ * used as-is (commit hash).
1512
+ */
1513
+ function looksLikeCommitSha(ref) {
1514
+ return /^[0-9a-f]{7,40}$/i.test(ref);
1515
+ }
1516
+ function createGitMirrorManager(opts) {
1517
+ const mirrorsRoot = join(opts.dataDir, "git-mirrors");
1518
+ const cloneTimeoutMs = opts.cloneTimeoutMs ?? Number(process.env.FIRST_TREE_HUB_GIT_CLONE_TIMEOUT_MS ?? DEFAULT_CLONE_TIMEOUT_MS);
1519
+ const log = opts.log ?? (() => {});
1520
+ const urlLocks = /* @__PURE__ */ new Map();
1521
+ function withUrlLock(url, op) {
1522
+ const key = hashUrl(url);
1523
+ const next = (urlLocks.get(key) ?? Promise.resolve()).then(op, op);
1524
+ urlLocks.set(key, next);
1525
+ next.then(() => {
1526
+ if (urlLocks.get(key) === next) urlLocks.delete(key);
1527
+ }, () => {
1528
+ if (urlLocks.get(key) === next) urlLocks.delete(key);
1529
+ });
1530
+ return next;
1531
+ }
1532
+ function mirrorDir(url) {
1533
+ return join(mirrorsRoot, hashUrl(url));
1534
+ }
1535
+ async function git(args, cwd, timeoutMs, env) {
1536
+ const start = Date.now();
1537
+ return await new Promise((resolveExec, rejectExec) => {
1538
+ const proc = spawn("git", args, {
1539
+ cwd: cwd ?? void 0,
1540
+ env: env ?? process.env,
1541
+ stdio: [
1542
+ "ignore",
1543
+ "pipe",
1544
+ "pipe"
1545
+ ]
1546
+ });
1547
+ let stdout = "";
1548
+ let stderr = "";
1549
+ proc.stdout.on("data", (d) => {
1550
+ stdout += String(d);
1551
+ });
1552
+ proc.stderr.on("data", (d) => {
1553
+ stderr += String(d);
1554
+ });
1555
+ const timer = setTimeout(() => {
1556
+ proc.kill("SIGKILL");
1557
+ rejectExec(new GitMirrorTimeoutError(`git ${args.join(" ")} timed out after ${timeoutMs}ms`));
1558
+ }, timeoutMs);
1559
+ proc.on("error", (err) => {
1560
+ clearTimeout(timer);
1561
+ rejectExec(err);
1562
+ });
1563
+ proc.on("close", (code) => {
1564
+ clearTimeout(timer);
1565
+ const elapsedMs = Date.now() - start;
1566
+ if (code === 0) resolveExec({
1567
+ stdout,
1568
+ stderr,
1569
+ elapsedMs
1570
+ });
1571
+ else rejectExec(new GitMirrorError(`git ${args.join(" ")} exited with code ${code}: ${stderr.slice(0, 1024)}`));
1572
+ });
1573
+ });
1574
+ }
1575
+ async function gitOk(args, cwd, timeoutMs) {
1576
+ try {
1577
+ await git(args, cwd, timeoutMs);
1578
+ return true;
1579
+ } catch {
1580
+ return false;
1581
+ }
1582
+ }
1583
+ /**
1584
+ * Bring the mirror's config to the invariant expected by this module:
1585
+ * fetch refspec = `+refs/heads/*:refs/remotes/origin/*`, `remote.origin.mirror`
1586
+ * absent, `refs/remotes/origin/HEAD` resolvable.
1587
+ *
1588
+ * Called from `ensureMirror` on every invocation — both the fresh-clone path
1589
+ * (ensures our own bootstrap wrote the right values) and the pre-existing
1590
+ * mirror path (repairs drift from the legacy `--mirror` config).
1591
+ */
1592
+ async function assertMirrorConfig(mirrorPath, url) {
1593
+ let migrated = false;
1594
+ let currentFetch = "";
1595
+ try {
1596
+ const { stdout } = await git([
1597
+ "config",
1598
+ "--get-all",
1599
+ "remote.origin.fetch"
1600
+ ], mirrorPath, 1e4);
1601
+ currentFetch = stdout.trim();
1602
+ } catch {
1603
+ currentFetch = "";
1604
+ }
1605
+ if (currentFetch !== FETCH_REFSPEC) {
1606
+ await git([
1607
+ "config",
1608
+ "--replace-all",
1609
+ "remote.origin.fetch",
1610
+ FETCH_REFSPEC
1611
+ ], mirrorPath, 1e4);
1612
+ migrated = true;
1613
+ }
1614
+ if (await gitOk([
1615
+ "config",
1616
+ "--get",
1617
+ "remote.origin.mirror"
1618
+ ], mirrorPath, 1e4)) {
1619
+ await git([
1620
+ "config",
1621
+ "--unset-all",
1622
+ "remote.origin.mirror"
1623
+ ], mirrorPath, 1e4);
1624
+ migrated = true;
1625
+ }
1626
+ try {
1627
+ const { stdout } = await git([
1628
+ "config",
1629
+ "--get",
1630
+ "remote.origin.url"
1631
+ ], mirrorPath, 1e4);
1632
+ if (stdout.trim() !== url) {
1633
+ await git([
1634
+ "config",
1635
+ "--replace-all",
1636
+ "remote.origin.url",
1637
+ url
1638
+ ], mirrorPath, 1e4);
1639
+ migrated = true;
1640
+ }
1641
+ } catch {
1642
+ await git([
1643
+ "remote",
1644
+ "add",
1645
+ "origin",
1646
+ url
1647
+ ], mirrorPath, 1e4);
1648
+ migrated = true;
1649
+ }
1650
+ if (migrated) {
1651
+ await git([
1652
+ "fetch",
1653
+ "--prune",
1654
+ "origin"
1655
+ ], mirrorPath, cloneTimeoutMs);
1656
+ await gitOk([
1657
+ "remote",
1658
+ "set-head",
1659
+ "origin",
1660
+ "--auto"
1661
+ ], mirrorPath, 3e4);
1662
+ log("mirrorConfigMigrated", { gitUrl: url });
1663
+ }
1664
+ return { migrated };
1665
+ }
1666
+ /**
1667
+ * Bootstrap a fresh mirror at `mirrorPath`. Uses `git init --bare` +
1668
+ * manual remote setup rather than `git clone --mirror` / `git clone --bare`,
1669
+ * so we never transiently have the mirror configured to force-write
1670
+ * `refs/heads/*` on fetch.
1671
+ */
1672
+ async function bootstrapMirror(mirrorPath, url) {
1673
+ mkdirSync(dirname(mirrorPath), { recursive: true });
1674
+ await git([
1675
+ "init",
1676
+ "--bare",
1677
+ mirrorPath
1678
+ ], null, cloneTimeoutMs);
1679
+ await git([
1680
+ "remote",
1681
+ "add",
1682
+ "origin",
1683
+ url
1684
+ ], mirrorPath, 1e4);
1685
+ await git([
1686
+ "config",
1687
+ "--replace-all",
1688
+ "remote.origin.fetch",
1689
+ FETCH_REFSPEC
1690
+ ], mirrorPath, 1e4);
1691
+ await git([
1692
+ "fetch",
1693
+ "--prune",
1694
+ "origin"
1695
+ ], mirrorPath, cloneTimeoutMs);
1696
+ await gitOk([
1697
+ "remote",
1698
+ "set-head",
1699
+ "origin",
1700
+ "--auto"
1701
+ ], mirrorPath, 3e4);
1702
+ }
1703
+ async function branchExists(mirrorPath, branchName) {
1704
+ return await gitOk([
1705
+ "rev-parse",
1706
+ "--verify",
1707
+ "--quiet",
1708
+ `refs/heads/${branchName}`
1709
+ ], mirrorPath, 1e4);
1710
+ }
1711
+ /**
1712
+ * Resolve the commit-ish to base a new session branch on.
1713
+ *
1714
+ * - explicit SHA → use as-is
1715
+ * - explicit branch name → prefer `refs/remotes/origin/<ref>`, fall back to
1716
+ * a literal SHA resolution in case the caller handed us a short commit
1717
+ * - `ref` absent → `refs/remotes/origin/HEAD`
1718
+ */
1719
+ async function resolveBase(mirrorPath, ref) {
1720
+ if (!ref) {
1721
+ if (await gitOk([
1722
+ "rev-parse",
1723
+ "--verify",
1724
+ "--quiet",
1725
+ "refs/remotes/origin/HEAD"
1726
+ ], mirrorPath, 1e4)) return "refs/remotes/origin/HEAD";
1727
+ throw new GitMirrorError("Cannot resolve default branch: refs/remotes/origin/HEAD is missing. Re-run with an explicit `ref`.");
1728
+ }
1729
+ if (looksLikeCommitSha(ref)) {
1730
+ if (await gitOk([
1731
+ "cat-file",
1732
+ "-e",
1733
+ ref
1734
+ ], mirrorPath, 1e4)) return ref;
1735
+ }
1736
+ const remoteRef = `refs/remotes/origin/${ref}`;
1737
+ if (await gitOk([
1738
+ "rev-parse",
1739
+ "--verify",
1740
+ "--quiet",
1741
+ remoteRef
1742
+ ], mirrorPath, 1e4)) return remoteRef;
1743
+ return ref;
1744
+ }
1745
+ return {
1746
+ get mirrorsRoot() {
1747
+ return mirrorsRoot;
1748
+ },
1749
+ ensureMirror(url) {
1750
+ return withUrlLock(url, async () => {
1751
+ mkdirSync(mirrorsRoot, { recursive: true });
1752
+ const path = mirrorDir(url);
1753
+ if (existsSync(join(path, "HEAD"))) {
1754
+ const { migrated } = await assertMirrorConfig(path, url);
1755
+ if (migrated) {}
1756
+ return {
1757
+ mirrorPath: path,
1758
+ elapsedMs: 0,
1759
+ cloned: false
1760
+ };
1761
+ }
1762
+ const start = Date.now();
1763
+ try {
1764
+ await bootstrapMirror(path, url);
1765
+ const elapsedMs = Date.now() - start;
1766
+ log("ensureMirror", {
1767
+ gitUrl: url,
1768
+ elapsedMs,
1769
+ cloned: true
1770
+ });
1771
+ return {
1772
+ mirrorPath: path,
1773
+ elapsedMs,
1774
+ cloned: true
1775
+ };
1776
+ } catch (err) {
1777
+ if (err instanceof GitMirrorTimeoutError) log("mirrorCloneTimeout", {
1778
+ gitUrl: url,
1779
+ timeoutMs: cloneTimeoutMs,
1780
+ elapsedMs: cloneTimeoutMs
1781
+ });
1782
+ if (existsSync(path)) rmSync(path, {
1783
+ recursive: true,
1784
+ force: true
1785
+ });
1786
+ throw err;
1787
+ }
1788
+ });
1789
+ },
1790
+ fetchMirror(url) {
1791
+ return withUrlLock(url, async () => {
1792
+ const path = mirrorDir(url);
1793
+ if (!existsSync(join(path, "HEAD"))) throw new GitMirrorError(`Cannot fetch — no mirror exists for "${url}"`);
1794
+ try {
1795
+ const { elapsedMs } = await git([
1796
+ "fetch",
1797
+ "--prune",
1798
+ "origin"
1799
+ ], path, cloneTimeoutMs);
1800
+ return { elapsedMs };
1801
+ } catch (err) {
1802
+ log("mirrorFetchFailed", {
1803
+ gitUrl: url,
1804
+ errorCode: err instanceof GitMirrorError ? "git-failed" : "unknown",
1805
+ stderr: err instanceof Error ? err.message.slice(0, 1024) : String(err).slice(0, 1024)
1806
+ });
1807
+ throw err;
1808
+ }
1809
+ });
1810
+ },
1811
+ createWorktree({ url, ref, targetPath, sessionKey }) {
1812
+ return withUrlLock(url, async () => {
1813
+ const mirror = mirrorDir(url);
1814
+ if (!existsSync(join(mirror, "HEAD"))) throw new GitMirrorError(`Cannot create worktree — no mirror exists for "${url}"`);
1815
+ const absTarget = resolve(targetPath);
1816
+ const branchName = deriveSessionBranchName(sessionKey, url);
1817
+ if (existsSync(absTarget) && !isHubManagedWorktree(absTarget)) {
1818
+ log("worktreeCreateConflict", {
1819
+ gitUrl: url,
1820
+ targetPath: absTarget,
1821
+ occupantKind: classifyOccupant(absTarget)
1822
+ });
1823
+ throw new GitMirrorWorktreeConflictError(`Worktree target "${absTarget}" is already occupied by ${classifyOccupant(absTarget)} — aborting (D13)`);
1824
+ }
1825
+ const pathExists = existsSync(absTarget);
1826
+ const hasBranch = await branchExists(mirror, branchName);
1827
+ mkdirSync(dirname(absTarget), { recursive: true });
1828
+ if (pathExists && hasBranch) {} else if (pathExists && !hasBranch) throw new GitMirrorError(`Worktree directory "${absTarget}" exists as a Hub worktree but the expected session branch "${branchName}" is missing in the mirror — manual cleanup required`);
1829
+ else if (!pathExists && hasBranch) await git([
1830
+ "worktree",
1831
+ "add",
1832
+ absTarget,
1833
+ branchName
1834
+ ], mirror, cloneTimeoutMs);
1835
+ else await git([
1836
+ "worktree",
1837
+ "add",
1838
+ "-b",
1839
+ branchName,
1840
+ absTarget,
1841
+ await resolveBase(mirror, ref)
1842
+ ], mirror, cloneTimeoutMs);
1843
+ return {
1844
+ worktreePath: absTarget,
1845
+ headCommit: (await git(["rev-parse", "HEAD"], absTarget, 3e4)).stdout.trim(),
1846
+ branchName
1847
+ };
1848
+ });
1849
+ },
1850
+ removeWorktree({ url, path, branchName }) {
1851
+ return withUrlLock(url, async () => {
1852
+ const absTarget = resolve(path);
1853
+ const mirror = mirrorDir(url);
1854
+ if (!isBareRepo(mirror)) {
1855
+ if (existsSync(absTarget)) rmSync(absTarget, {
1856
+ recursive: true,
1857
+ force: true
1858
+ });
1859
+ return;
1860
+ }
1861
+ if (existsSync(absTarget)) await gitOk([
1862
+ "worktree",
1863
+ "remove",
1864
+ "--force",
1865
+ absTarget
1866
+ ], mirror, 3e4);
1867
+ else await gitOk(["worktree", "prune"], mirror, 3e4);
1868
+ if (existsSync(absTarget)) rmSync(absTarget, {
1869
+ recursive: true,
1870
+ force: true
1871
+ });
1872
+ if (await branchExists(mirror, branchName)) await gitOk([
1873
+ "branch",
1874
+ "-D",
1875
+ branchName
1876
+ ], mirror, 1e4);
1877
+ });
1878
+ },
1879
+ async gcMirrors(stillReferencedUrls) {
1880
+ if (!existsSync(mirrorsRoot)) return { removed: [] };
1881
+ const wantedHashes = new Set([...stillReferencedUrls].map(hashUrl));
1882
+ const removed = [];
1883
+ for (const entry of readdirSync(mirrorsRoot)) {
1884
+ if (wantedHashes.has(entry)) continue;
1885
+ const path = join(mirrorsRoot, entry);
1886
+ if (!isBareRepo(path)) continue;
1887
+ rmSync(path, {
1888
+ recursive: true,
1889
+ force: true
1890
+ });
1891
+ removed.push(entry);
1892
+ }
1893
+ return { removed };
1894
+ }
1895
+ };
1896
+ }
1897
+ function isBareRepo(p) {
1898
+ return existsSync(join(p, "HEAD")) && existsSync(join(p, "objects"));
1899
+ }
1900
+ function isHubManagedWorktree(p) {
1901
+ const gitMarker = join(p, ".git");
1902
+ if (!existsSync(gitMarker)) return false;
1903
+ try {
1904
+ return statSync(gitMarker).isFile();
1905
+ } catch {
1906
+ return false;
1907
+ }
1908
+ }
1909
+ function classifyOccupant(p) {
1910
+ try {
1911
+ const stat = statSync(p);
1912
+ if (stat.isSymbolicLink()) return "symlink";
1913
+ if (stat.isDirectory()) {
1914
+ if (existsSync(join(p, ".git"))) return "git-repo";
1915
+ return "directory";
1916
+ }
1917
+ if (stat.isFile()) return "file";
1918
+ return "other";
1919
+ } catch {
1920
+ return "unknown";
1921
+ }
1922
+ }
1923
+ var GitMirrorError = class extends Error {
1924
+ constructor(message) {
1925
+ super(message);
1926
+ this.name = "GitMirrorError";
1927
+ }
1928
+ };
1929
+ var GitMirrorTimeoutError = class extends GitMirrorError {
1930
+ constructor(message) {
1931
+ super(message);
1932
+ this.name = "GitMirrorTimeoutError";
1933
+ }
1934
+ };
1935
+ var GitMirrorWorktreeConflictError = class extends GitMirrorError {
1936
+ constructor(message) {
1937
+ super(message);
1938
+ this.name = "GitMirrorWorktreeConflictError";
1939
+ }
1940
+ };
1497
1941
  /**
1498
1942
  * InputController — push-based async iterable bridge.
1499
1943
  *
@@ -1742,7 +2186,7 @@ const createClaudeCodeHandler = (config) => {
1742
2186
  let appliedConfigVersion = 0;
1743
2187
  let appliedModel = "";
1744
2188
  let appliedPayload = null;
1745
- /** Worktree paths materialised for this session — removed on shutdown. */
2189
+ /** Worktrees materialised for this session — each entry removed on shutdown. */
1746
2190
  const ownedWorktrees = [];
1747
2191
  function toSDKUserMessage(message, sessionId) {
1748
2192
  const rawContent = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
@@ -1966,28 +2410,37 @@ const createClaudeCodeHandler = (config) => {
1966
2410
  await gitMirrorManager.fetchMirror(repo.url);
1967
2411
  if (existsSync(targetPath) && isHubWorktreeMarker(targetPath)) {
1968
2412
  sessionCtx.log(`Git: reusing existing worktree at ${localPath}`);
1969
- ownedWorktrees.push(targetPath);
2413
+ ownedWorktrees.push({
2414
+ url: repo.url,
2415
+ path: targetPath,
2416
+ branchName: deriveSessionBranchName(sessionCtx.chatId, repo.url)
2417
+ });
1970
2418
  continue;
1971
2419
  }
1972
- const { headCommit } = await gitMirrorManager.createWorktree({
2420
+ const { headCommit, branchName } = await gitMirrorManager.createWorktree({
1973
2421
  url: repo.url,
1974
2422
  ref: repo.ref,
1975
- targetPath
2423
+ targetPath,
2424
+ sessionKey: sessionCtx.chatId
1976
2425
  });
1977
- ownedWorktrees.push(targetPath);
1978
- sessionCtx.log(`Git: worktree at ${localPath} @ ${headCommit.slice(0, 7)}`);
2426
+ ownedWorktrees.push({
2427
+ url: repo.url,
2428
+ path: targetPath,
2429
+ branchName
2430
+ });
2431
+ sessionCtx.log(`Git: worktree at ${localPath} @ ${headCommit.slice(0, 7)} on ${branchName}`);
1979
2432
  }
1980
2433
  }
1981
2434
  /** Tear down all worktrees this session owns; best-effort. */
1982
2435
  async function cleanupGitWorktrees(sessionCtx) {
1983
2436
  if (!gitMirrorManager) return;
1984
2437
  while (ownedWorktrees.length > 0) {
1985
- const path = ownedWorktrees.pop();
1986
- if (!path) continue;
2438
+ const entry = ownedWorktrees.pop();
2439
+ if (!entry) continue;
1987
2440
  try {
1988
- await gitMirrorManager.removeWorktree(path);
2441
+ await gitMirrorManager.removeWorktree(entry);
1989
2442
  } catch (err) {
1990
- sessionCtx.log(`Git: removeWorktree(${path}) failed — ${err instanceof Error ? err.message : String(err)}`);
2443
+ sessionCtx.log(`Git: removeWorktree(${entry.path}) failed — ${err instanceof Error ? err.message : String(err)}`);
1991
2444
  }
1992
2445
  }
1993
2446
  }
@@ -2201,226 +2654,6 @@ function createAgentConfigCache(opts) {
2201
2654
  }
2202
2655
  };
2203
2656
  }
2204
- const DEFAULT_CLONE_TIMEOUT_MS = 300 * 1e3;
2205
- function hashUrl(url) {
2206
- return createHash("sha256").update(url).digest("hex").slice(0, 32);
2207
- }
2208
- function createGitMirrorManager(opts) {
2209
- const mirrorsRoot = join(opts.dataDir, "git-mirrors");
2210
- const cloneTimeoutMs = opts.cloneTimeoutMs ?? Number(process.env.FIRST_TREE_HUB_GIT_CLONE_TIMEOUT_MS ?? DEFAULT_CLONE_TIMEOUT_MS);
2211
- const log = opts.log ?? (() => {});
2212
- function mirrorDir(url) {
2213
- return join(mirrorsRoot, hashUrl(url));
2214
- }
2215
- async function git(args, cwd, timeoutMs, env) {
2216
- const start = Date.now();
2217
- return await new Promise((resolveExec, rejectExec) => {
2218
- const proc = spawn("git", args, {
2219
- cwd: cwd ?? void 0,
2220
- env: env ?? process.env,
2221
- stdio: [
2222
- "ignore",
2223
- "pipe",
2224
- "pipe"
2225
- ]
2226
- });
2227
- let stdout = "";
2228
- let stderr = "";
2229
- proc.stdout.on("data", (d) => {
2230
- stdout += String(d);
2231
- });
2232
- proc.stderr.on("data", (d) => {
2233
- stderr += String(d);
2234
- });
2235
- const timer = setTimeout(() => {
2236
- proc.kill("SIGKILL");
2237
- rejectExec(new GitMirrorTimeoutError(`git ${args.join(" ")} timed out after ${timeoutMs}ms`));
2238
- }, timeoutMs);
2239
- proc.on("error", (err) => {
2240
- clearTimeout(timer);
2241
- rejectExec(err);
2242
- });
2243
- proc.on("close", (code) => {
2244
- clearTimeout(timer);
2245
- const elapsedMs = Date.now() - start;
2246
- if (code === 0) resolveExec({
2247
- stdout,
2248
- stderr,
2249
- elapsedMs
2250
- });
2251
- else rejectExec(new GitMirrorError(`git ${args.join(" ")} exited with code ${code}: ${stderr.slice(0, 1024)}`));
2252
- });
2253
- });
2254
- }
2255
- return {
2256
- get mirrorsRoot() {
2257
- return mirrorsRoot;
2258
- },
2259
- async ensureMirror(url) {
2260
- mkdirSync(mirrorsRoot, { recursive: true });
2261
- const path = mirrorDir(url);
2262
- if (existsSync(join(path, "HEAD"))) return {
2263
- mirrorPath: path,
2264
- elapsedMs: 0,
2265
- cloned: false
2266
- };
2267
- try {
2268
- const { elapsedMs } = await git([
2269
- "clone",
2270
- "--mirror",
2271
- url,
2272
- path
2273
- ], null, cloneTimeoutMs);
2274
- log("ensureMirror", {
2275
- gitUrl: url,
2276
- elapsedMs,
2277
- cloned: true
2278
- });
2279
- return {
2280
- mirrorPath: path,
2281
- elapsedMs,
2282
- cloned: true
2283
- };
2284
- } catch (err) {
2285
- if (err instanceof GitMirrorTimeoutError) log("mirrorCloneTimeout", {
2286
- gitUrl: url,
2287
- timeoutMs: cloneTimeoutMs,
2288
- elapsedMs: cloneTimeoutMs
2289
- });
2290
- if (existsSync(path)) rmSync(path, {
2291
- recursive: true,
2292
- force: true
2293
- });
2294
- throw err;
2295
- }
2296
- },
2297
- async fetchMirror(url) {
2298
- const path = mirrorDir(url);
2299
- if (!existsSync(join(path, "HEAD"))) throw new GitMirrorError(`Cannot fetch — no mirror exists for "${url}"`);
2300
- try {
2301
- const { elapsedMs } = await git(["fetch", "--prune"], path, cloneTimeoutMs);
2302
- return { elapsedMs };
2303
- } catch (err) {
2304
- log("mirrorFetchFailed", {
2305
- gitUrl: url,
2306
- errorCode: err instanceof GitMirrorError ? "git-failed" : "unknown",
2307
- stderr: err instanceof Error ? err.message.slice(0, 1024) : String(err).slice(0, 1024)
2308
- });
2309
- throw err;
2310
- }
2311
- },
2312
- async createWorktree({ url, ref, targetPath }) {
2313
- const mirror = mirrorDir(url);
2314
- if (!existsSync(join(mirror, "HEAD"))) throw new GitMirrorError(`Cannot create worktree — no mirror exists for "${url}"`);
2315
- const absTarget = resolve(targetPath);
2316
- if (existsSync(absTarget) && !isHubManagedWorktree(absTarget)) {
2317
- log("worktreeCreateConflict", {
2318
- gitUrl: url,
2319
- targetPath: absTarget,
2320
- occupantKind: classifyOccupant(absTarget)
2321
- });
2322
- throw new GitMirrorWorktreeConflictError(`Worktree target "${absTarget}" is already occupied by ${classifyOccupant(absTarget)} — aborting (D13)`);
2323
- }
2324
- mkdirSync(dirname(absTarget), { recursive: true });
2325
- const args = [
2326
- "worktree",
2327
- "add",
2328
- "--detach",
2329
- absTarget
2330
- ];
2331
- if (ref) args.push(ref);
2332
- await git(args, mirror, cloneTimeoutMs);
2333
- return {
2334
- worktreePath: absTarget,
2335
- headCommit: (await git(["rev-parse", "HEAD"], absTarget, 3e4)).stdout.trim()
2336
- };
2337
- },
2338
- async removeWorktree(path) {
2339
- const absTarget = resolve(path);
2340
- if (!existsSync(absTarget)) return;
2341
- if (!existsSync(mirrorsRoot)) return;
2342
- let removed = false;
2343
- for (const entry of readdirSync(mirrorsRoot)) {
2344
- const mirror = join(mirrorsRoot, entry);
2345
- if (!isBareRepo(mirror)) continue;
2346
- try {
2347
- await git([
2348
- "worktree",
2349
- "remove",
2350
- "--force",
2351
- absTarget
2352
- ], mirror, 3e4);
2353
- removed = true;
2354
- break;
2355
- } catch {}
2356
- }
2357
- if (!removed && existsSync(absTarget)) rmSync(absTarget, {
2358
- recursive: true,
2359
- force: true
2360
- });
2361
- },
2362
- async gcMirrors(stillReferencedUrls) {
2363
- if (!existsSync(mirrorsRoot)) return { removed: [] };
2364
- const wantedHashes = new Set([...stillReferencedUrls].map(hashUrl));
2365
- const removed = [];
2366
- for (const entry of readdirSync(mirrorsRoot)) {
2367
- if (wantedHashes.has(entry)) continue;
2368
- const path = join(mirrorsRoot, entry);
2369
- if (!isBareRepo(path)) continue;
2370
- rmSync(path, {
2371
- recursive: true,
2372
- force: true
2373
- });
2374
- removed.push(entry);
2375
- }
2376
- return { removed };
2377
- }
2378
- };
2379
- }
2380
- function isBareRepo(p) {
2381
- return existsSync(join(p, "HEAD")) && existsSync(join(p, "objects"));
2382
- }
2383
- function isHubManagedWorktree(p) {
2384
- const gitMarker = join(p, ".git");
2385
- if (!existsSync(gitMarker)) return false;
2386
- try {
2387
- return statSync(gitMarker).isFile();
2388
- } catch {
2389
- return false;
2390
- }
2391
- }
2392
- function classifyOccupant(p) {
2393
- try {
2394
- const stat = statSync(p);
2395
- if (stat.isSymbolicLink()) return "symlink";
2396
- if (stat.isDirectory()) {
2397
- if (existsSync(join(p, ".git"))) return "git-repo";
2398
- return "directory";
2399
- }
2400
- if (stat.isFile()) return "file";
2401
- return "other";
2402
- } catch {
2403
- return "unknown";
2404
- }
2405
- }
2406
- var GitMirrorError = class extends Error {
2407
- constructor(message) {
2408
- super(message);
2409
- this.name = "GitMirrorError";
2410
- }
2411
- };
2412
- var GitMirrorTimeoutError = class extends GitMirrorError {
2413
- constructor(message) {
2414
- super(message);
2415
- this.name = "GitMirrorTimeoutError";
2416
- }
2417
- };
2418
- var GitMirrorWorktreeConflictError = class extends GitMirrorError {
2419
- constructor(message) {
2420
- super(message);
2421
- this.name = "GitMirrorWorktreeConflictError";
2422
- }
2423
- };
2424
2657
  /**
2425
2658
  * Deduplicator — bounded set of recently seen IDs.
2426
2659
  *
@@ -4114,7 +4347,7 @@ function setNestedByDot(obj, dotPath, value) {
4114
4347
  if (lastKey !== void 0) current[lastKey] = value;
4115
4348
  }
4116
4349
  //#endregion
4117
- //#region ../server/dist/app-nJ9jSQtv.mjs
4350
+ //#region ../server/dist/app-CWKBBGod.mjs
4118
4351
  var __defProp = Object.defineProperty;
4119
4352
  var __exportAll = (all, no_symbols) => {
4120
4353
  let target = {};
@@ -4649,8 +4882,17 @@ function parseId$1(raw) {
4649
4882
  return id;
4650
4883
  }
4651
4884
  async function adminAdapterMappingRoutes(app) {
4652
- app.get("/", async () => {
4653
- return (await app.db.select().from(adapterAgentMappings).orderBy(desc(adapterAgentMappings.createdAt))).map((r) => ({
4885
+ app.get("/", async (request) => {
4886
+ const scope = memberScope(request);
4887
+ return (await app.db.select({
4888
+ id: adapterAgentMappings.id,
4889
+ platform: adapterAgentMappings.platform,
4890
+ externalUserId: adapterAgentMappings.externalUserId,
4891
+ agentId: adapterAgentMappings.agentId,
4892
+ boundVia: adapterAgentMappings.boundVia,
4893
+ displayName: adapterAgentMappings.displayName,
4894
+ createdAt: adapterAgentMappings.createdAt
4895
+ }).from(adapterAgentMappings).innerJoin(agents, eq(agents.uuid, adapterAgentMappings.agentId)).where(eq(agents.organizationId, scope.organizationId)).orderBy(desc(adapterAgentMappings.createdAt))).map((r) => ({
4654
4896
  id: r.id,
4655
4897
  platform: r.platform,
4656
4898
  externalUserId: r.externalUserId,
@@ -5077,8 +5319,23 @@ async function createAgent(db, data) {
5077
5319
  const name = data.name ?? null;
5078
5320
  if (name?.startsWith(RESERVED_AGENT_NAME_PREFIX)) throw new BadRequestError(`Agent name "${name}" is reserved — names starting with "${RESERVED_AGENT_NAME_PREFIX}" are Hub-internal`);
5079
5321
  const inboxId = `inbox_${uuid}`;
5080
- const orgId = data.organizationId ?? await resolveDefaultOrgId(db);
5081
- const managerId = data.managerId ?? await resolveFallbackManagerId(db, orgId);
5322
+ let orgId;
5323
+ let managerId;
5324
+ if (data.managerId && data.organizationId) {
5325
+ orgId = data.organizationId;
5326
+ managerId = data.managerId;
5327
+ } else if (data.managerId) {
5328
+ const [manager] = await db.select({
5329
+ id: members.id,
5330
+ organizationId: members.organizationId
5331
+ }).from(members).where(eq(members.id, data.managerId)).limit(1);
5332
+ if (!manager) throw new BadRequestError(`Manager "${data.managerId}" not found`);
5333
+ orgId = manager.organizationId;
5334
+ managerId = manager.id;
5335
+ } else {
5336
+ orgId = data.organizationId ?? await resolveDefaultOrgId(db);
5337
+ managerId = await resolveFallbackManagerId(db, orgId);
5338
+ }
5082
5339
  const clientId = await resolveAgentClient(db, {
5083
5340
  clientId: data.clientId,
5084
5341
  managerId,
@@ -11044,4 +11301,353 @@ function resolveWebDist() {
11044
11301
  } catch {}
11045
11302
  }
11046
11303
  //#endregion
11047
- export { SdkError as A, ensurePostgres as C, createOwner as D, ClientRuntime as E, cleanWorkspaces as M, hasUser as O, status as S, stopPostgres as T, checkServerHealth as _, formatCheckReport as a, printResults as b, onboardCreate as c, checkAgentConfigs as d, checkClientConfig as f, checkServerConfig as g, checkNodeVersion as h, promptMissingFields as i, SessionRegistry as j, FirstTreeHubSDK as k, saveOnboardState as l, checkDocker as m, isInteractive as n, loadOnboardState as o, checkDatabase as p, promptAddAgent as r, onboardCheck as s, startServer as t, runMigrations as u, checkServerReachable as v, isDockerAvailable as w, blank as x, checkWebSocket as y };
11304
+ //#region src/core/service-install.ts
11305
+ const LAUNCHD_LABEL = "dev.first-tree-hub.client";
11306
+ const SYSTEMD_UNIT = "first-tree-hub-client.service";
11307
+ const LOG_DIR = join(DEFAULT_HOME_DIR$1, "logs");
11308
+ function whichBin(name) {
11309
+ try {
11310
+ return execFileSync(process.platform === "win32" ? "where" : "which", [name], {
11311
+ encoding: "utf-8",
11312
+ timeout: 3e3
11313
+ }).split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0] ?? null;
11314
+ } catch {
11315
+ return null;
11316
+ }
11317
+ }
11318
+ /**
11319
+ * Resolve how the service should launch the CLI.
11320
+ *
11321
+ * Prefers the installed `first-tree-hub` bin on PATH (usually a shim under
11322
+ * /usr/local/bin or ~/.npm-global/bin). Falls back to invoking the current
11323
+ * Node interpreter against the running script (handles `pnpm dev`, tsx, and
11324
+ * dev-only global installs).
11325
+ */
11326
+ function resolveCliInvocation() {
11327
+ const bin = whichBin("first-tree-hub");
11328
+ if (bin && isAbsolute(bin)) try {
11329
+ return {
11330
+ kind: "bin",
11331
+ program: realpathSync(bin)
11332
+ };
11333
+ } catch {
11334
+ return {
11335
+ kind: "bin",
11336
+ program: bin
11337
+ };
11338
+ }
11339
+ const script = process.argv[1];
11340
+ if (!script) throw new Error("Cannot resolve CLI entry point (process.argv[1] is empty).");
11341
+ const scriptAbs = isAbsolute(script) ? script : join(process.cwd(), script);
11342
+ return {
11343
+ kind: "node",
11344
+ program: process.execPath,
11345
+ args: [scriptAbs]
11346
+ };
11347
+ }
11348
+ function ensureLogDir() {
11349
+ mkdirSync(LOG_DIR, {
11350
+ recursive: true,
11351
+ mode: 448
11352
+ });
11353
+ }
11354
+ function launchdPlistPath() {
11355
+ return join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
11356
+ }
11357
+ function renderPlist(invocation) {
11358
+ const argsXml = (invocation.kind === "bin" ? [
11359
+ invocation.program,
11360
+ "client",
11361
+ "start",
11362
+ "--no-interactive"
11363
+ ] : [
11364
+ invocation.program,
11365
+ ...invocation.args,
11366
+ "client",
11367
+ "start",
11368
+ "--no-interactive"
11369
+ ]).map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
11370
+ const outLog = join(LOG_DIR, "client.out.log");
11371
+ const errLog = join(LOG_DIR, "client.err.log");
11372
+ return `<?xml version="1.0" encoding="UTF-8"?>
11373
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTD/PropertyList-1.0.dtd">
11374
+ <plist version="1.0">
11375
+ <dict>
11376
+ <key>Label</key>
11377
+ <string>${LAUNCHD_LABEL}</string>
11378
+ <key>ProgramArguments</key>
11379
+ <array>
11380
+ ${argsXml}
11381
+ </array>
11382
+ <key>EnvironmentVariables</key>
11383
+ <dict>
11384
+ <key>PATH</key>
11385
+ <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
11386
+ </dict>
11387
+ <key>RunAtLoad</key>
11388
+ <true/>
11389
+ <key>KeepAlive</key>
11390
+ <dict>
11391
+ <key>SuccessfulExit</key>
11392
+ <false/>
11393
+ </dict>
11394
+ <key>ThrottleInterval</key>
11395
+ <integer>10</integer>
11396
+ <key>StandardOutPath</key>
11397
+ <string>${escapeXml(outLog)}</string>
11398
+ <key>StandardErrorPath</key>
11399
+ <string>${escapeXml(errLog)}</string>
11400
+ </dict>
11401
+ </plist>
11402
+ `;
11403
+ }
11404
+ function escapeXml(value) {
11405
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
11406
+ }
11407
+ function launchctlDomainTarget() {
11408
+ return `gui/${userInfo().uid}`;
11409
+ }
11410
+ function launchdState() {
11411
+ if (!existsSync(launchdPlistPath())) return { state: "not-installed" };
11412
+ try {
11413
+ const out = execFileSync("launchctl", ["print", `${launchctlDomainTarget()}/${LAUNCHD_LABEL}`], {
11414
+ encoding: "utf-8",
11415
+ timeout: 5e3
11416
+ });
11417
+ const stateLine = out.split(/\r?\n/).find((l) => l.trim().startsWith("state ="));
11418
+ const pidLine = out.split(/\r?\n/).find((l) => l.trim().startsWith("pid ="));
11419
+ if (stateLine?.includes("running")) {
11420
+ const pid = pidLine?.split("=")[1]?.trim();
11421
+ return {
11422
+ state: "active",
11423
+ detail: pid ? `pid ${pid}` : "running"
11424
+ };
11425
+ }
11426
+ return {
11427
+ state: "inactive",
11428
+ detail: stateLine?.trim() ?? "loaded"
11429
+ };
11430
+ } catch {
11431
+ return {
11432
+ state: "inactive",
11433
+ detail: "plist present but not loaded"
11434
+ };
11435
+ }
11436
+ }
11437
+ function installLaunchd() {
11438
+ const invocation = resolveCliInvocation();
11439
+ ensureLogDir();
11440
+ const plistPath = launchdPlistPath();
11441
+ mkdirSync(dirname(plistPath), { recursive: true });
11442
+ writeFileSync(plistPath, renderPlist(invocation), { mode: 420 });
11443
+ const target = launchctlDomainTarget();
11444
+ try {
11445
+ execFileSync("launchctl", ["bootout", `${target}/${LAUNCHD_LABEL}`], {
11446
+ stdio: "ignore",
11447
+ timeout: 5e3
11448
+ });
11449
+ } catch {}
11450
+ execFileSync("launchctl", [
11451
+ "bootstrap",
11452
+ target,
11453
+ plistPath
11454
+ ], {
11455
+ stdio: "ignore",
11456
+ timeout: 5e3
11457
+ });
11458
+ execFileSync("launchctl", ["enable", `${target}/${LAUNCHD_LABEL}`], {
11459
+ stdio: "ignore",
11460
+ timeout: 5e3
11461
+ });
11462
+ const { state, detail } = launchdState();
11463
+ return {
11464
+ platform: "launchd",
11465
+ label: LAUNCHD_LABEL,
11466
+ unitPath: plistPath,
11467
+ logDir: LOG_DIR,
11468
+ state,
11469
+ detail
11470
+ };
11471
+ }
11472
+ function uninstallLaunchd() {
11473
+ const plistPath = launchdPlistPath();
11474
+ const target = launchctlDomainTarget();
11475
+ try {
11476
+ execFileSync("launchctl", ["bootout", `${target}/${LAUNCHD_LABEL}`], {
11477
+ stdio: "ignore",
11478
+ timeout: 5e3
11479
+ });
11480
+ } catch {}
11481
+ if (existsSync(plistPath)) rmSync(plistPath);
11482
+ return {
11483
+ platform: "launchd",
11484
+ label: LAUNCHD_LABEL,
11485
+ unitPath: plistPath,
11486
+ logDir: LOG_DIR,
11487
+ state: "not-installed"
11488
+ };
11489
+ }
11490
+ function systemdUnitPath() {
11491
+ return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "systemd", "user", SYSTEMD_UNIT);
11492
+ }
11493
+ function renderSystemdUnit(invocation) {
11494
+ return `[Unit]
11495
+ Description=First Tree Hub Client
11496
+ After=network-online.target
11497
+ Wants=network-online.target
11498
+
11499
+ [Service]
11500
+ Type=simple
11501
+ ExecStart=${invocation.kind === "bin" ? `${shellQuote(invocation.program)} client start --no-interactive` : `${shellQuote(invocation.program)} ${invocation.args.map(shellQuote).join(" ")} client start --no-interactive`}
11502
+ Restart=always
11503
+ RestartSec=10
11504
+ StandardOutput=append:${join(LOG_DIR, "client.out.log")}
11505
+ StandardError=append:${join(LOG_DIR, "client.err.log")}
11506
+ Environment=PATH=/usr/local/bin:/usr/bin:/bin
11507
+
11508
+ [Install]
11509
+ WantedBy=default.target
11510
+ `;
11511
+ }
11512
+ function shellQuote(value) {
11513
+ if (/^[A-Za-z0-9_\-./:=]+$/.test(value)) return value;
11514
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
11515
+ }
11516
+ function systemdState() {
11517
+ if (!existsSync(systemdUnitPath())) return { state: "not-installed" };
11518
+ try {
11519
+ const out = execFileSync("systemctl", [
11520
+ "--user",
11521
+ "is-active",
11522
+ SYSTEMD_UNIT
11523
+ ], {
11524
+ encoding: "utf-8",
11525
+ timeout: 5e3
11526
+ }).trim();
11527
+ if (out === "active") return {
11528
+ state: "active",
11529
+ detail: "running"
11530
+ };
11531
+ return {
11532
+ state: "inactive",
11533
+ detail: out
11534
+ };
11535
+ } catch (err) {
11536
+ return {
11537
+ state: "inactive",
11538
+ detail: (typeof err.stdout === "string" ? (err.stdout ?? "").trim() : "") || "unit present but not active"
11539
+ };
11540
+ }
11541
+ }
11542
+ function installSystemd() {
11543
+ const invocation = resolveCliInvocation();
11544
+ ensureLogDir();
11545
+ const unitPath = systemdUnitPath();
11546
+ mkdirSync(dirname(unitPath), { recursive: true });
11547
+ writeFileSync(unitPath, renderSystemdUnit(invocation), { mode: 420 });
11548
+ execFileSync("systemctl", ["--user", "daemon-reload"], {
11549
+ stdio: "ignore",
11550
+ timeout: 5e3
11551
+ });
11552
+ execFileSync("systemctl", [
11553
+ "--user",
11554
+ "enable",
11555
+ "--now",
11556
+ SYSTEMD_UNIT
11557
+ ], {
11558
+ stdio: "ignore",
11559
+ timeout: 1e4
11560
+ });
11561
+ const { state, detail } = systemdState();
11562
+ return {
11563
+ platform: "systemd",
11564
+ label: SYSTEMD_UNIT,
11565
+ unitPath,
11566
+ logDir: LOG_DIR,
11567
+ state,
11568
+ detail
11569
+ };
11570
+ }
11571
+ function uninstallSystemd() {
11572
+ const unitPath = systemdUnitPath();
11573
+ try {
11574
+ execFileSync("systemctl", [
11575
+ "--user",
11576
+ "disable",
11577
+ "--now",
11578
+ SYSTEMD_UNIT
11579
+ ], {
11580
+ stdio: "ignore",
11581
+ timeout: 1e4
11582
+ });
11583
+ } catch {}
11584
+ if (existsSync(unitPath)) rmSync(unitPath);
11585
+ try {
11586
+ execFileSync("systemctl", ["--user", "daemon-reload"], {
11587
+ stdio: "ignore",
11588
+ timeout: 5e3
11589
+ });
11590
+ } catch {}
11591
+ return {
11592
+ platform: "systemd",
11593
+ label: SYSTEMD_UNIT,
11594
+ unitPath,
11595
+ logDir: LOG_DIR,
11596
+ state: "not-installed"
11597
+ };
11598
+ }
11599
+ /** Is background-service install supported on the current platform? */
11600
+ function isServiceSupported() {
11601
+ return process.platform === "darwin" || process.platform === "linux";
11602
+ }
11603
+ /**
11604
+ * Install the background service for the current platform.
11605
+ *
11606
+ * @throws {Error} if the platform is not supported or the service manager fails.
11607
+ */
11608
+ function installClientService() {
11609
+ if (process.platform === "darwin") return installLaunchd();
11610
+ if (process.platform === "linux") return installSystemd();
11611
+ throw new Error(`Background service install is not supported on ${process.platform}. Run \`first-tree-hub client start\` manually to keep the computer online.`);
11612
+ }
11613
+ /** Report the current service state without modifying anything. */
11614
+ function getClientServiceStatus() {
11615
+ if (process.platform === "darwin") {
11616
+ const { state, detail } = launchdState();
11617
+ return {
11618
+ platform: "launchd",
11619
+ label: LAUNCHD_LABEL,
11620
+ unitPath: launchdPlistPath(),
11621
+ logDir: LOG_DIR,
11622
+ state,
11623
+ detail
11624
+ };
11625
+ }
11626
+ if (process.platform === "linux") {
11627
+ const { state, detail } = systemdState();
11628
+ return {
11629
+ platform: "systemd",
11630
+ label: SYSTEMD_UNIT,
11631
+ unitPath: systemdUnitPath(),
11632
+ logDir: LOG_DIR,
11633
+ state,
11634
+ detail
11635
+ };
11636
+ }
11637
+ return {
11638
+ platform: "unsupported",
11639
+ label: "",
11640
+ unitPath: "",
11641
+ logDir: LOG_DIR,
11642
+ state: "not-installed",
11643
+ detail: `platform ${process.platform} not supported`
11644
+ };
11645
+ }
11646
+ /** Uninstall the background service. No-op if not installed. */
11647
+ function uninstallClientService() {
11648
+ if (process.platform === "darwin") return uninstallLaunchd();
11649
+ if (process.platform === "linux") return uninstallSystemd();
11650
+ return getClientServiceStatus();
11651
+ }
11652
+ //#endregion
11653
+ export { stopPostgres as A, checkServerReachable as C, status as D, blank as E, SdkError as F, SessionRegistry as I, cleanWorkspaces as L, createOwner as M, hasUser as N, ensurePostgres as O, FirstTreeHubSDK as P, checkServerHealth as S, printResults as T, checkClientConfig as _, uninstallClientService as a, checkNodeVersion as b, promptAddAgent as c, loadOnboardState as d, onboardCheck as f, checkAgentConfigs as g, runMigrations as h, resolveCliInvocation as i, ClientRuntime as j, isDockerAvailable as k, promptMissingFields as l, saveOnboardState as m, installClientService as n, startServer as o, onboardCreate as p, isServiceSupported as r, isInteractive as s, getClientServiceStatus as t, formatCheckReport as u, checkDatabase as v, checkWebSocket as w, checkServerConfig as x, checkDocker as y };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  import { a as resolveAccessToken, n as ensureFreshAccessToken, o as resolveServerUrl, r as ensureFreshAdminToken } from "./bootstrap-CRDR6NwE.mjs";
2
- import { A as SdkError, C as ensurePostgres, D as createOwner, E as ClientRuntime, O as hasUser, S as status, T as stopPostgres, _ as checkServerHealth, a as formatCheckReport, b as printResults, c as onboardCreate, d as checkAgentConfigs, f as checkClientConfig, g as checkServerConfig, h as checkNodeVersion, i as promptMissingFields, k as FirstTreeHubSDK, m as checkDocker, n as isInteractive, p as checkDatabase, r as promptAddAgent, s as onboardCheck, t as startServer, u as runMigrations, v as checkServerReachable, w as isDockerAvailable, x as blank, y as checkWebSocket } from "./core-4nvleGlC.mjs";
2
+ import { A as stopPostgres, C as checkServerReachable, D as status, E as blank, F as SdkError, M as createOwner, N as hasUser, O as ensurePostgres, P as FirstTreeHubSDK, S as checkServerHealth, T as printResults, _ as checkClientConfig, a as uninstallClientService, b as checkNodeVersion, c as promptAddAgent, f as onboardCheck, g as checkAgentConfigs, h as runMigrations, i as resolveCliInvocation, j as ClientRuntime, k as isDockerAvailable, l as promptMissingFields, n as installClientService, o as startServer, p as onboardCreate, r as isServiceSupported, s as isInteractive, t as getClientServiceStatus, u as formatCheckReport, v as checkDatabase, w as checkWebSocket, x as checkServerConfig, y as checkDocker } from "./core-6-paFwyo.mjs";
3
3
  import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-CJ08ntOD.mjs";
4
- export { ClientRuntime, FirstTreeHubSDK, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, hasUser, isDockerAvailable, isInteractive, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveServerUrl, runMigrations, startServer, status, stopPostgres };
4
+ export { ClientRuntime, FirstTreeHubSDK, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, getClientServiceStatus, hasUser, installClientService, isDockerAvailable, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, runMigrations, startServer, status, stopPostgres, uninstallClientService };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-team-foundation/first-tree-hub",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "type": "module",
5
5
  "description": "First Tree Hub — unified CLI for server, client, and agent management",
6
6
  "exports": {