@agent-team-foundation/first-tree-hub 0.8.2 → 0.8.4

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,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import "../logger-core-2yeIU1fc-B-__AsQO.mjs";
3
- 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-99vUYmLs.mjs";
3
+ import { C as setConfigValue, S as serverConfigSchema, _ as loadAgents, b as resetConfigMeta, c as saveCredentials, 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, v as readConfigFile, y as resetConfig } from "../bootstrap-99vUYmLs.mjs";
4
4
  import "../observability-CJzDFY_G-CmvgUuzc.mjs";
5
- import { A as stopPostgres, C as checkServerReachable, F as SdkError, I as SessionRegistry, L as cleanWorkspaces, M as createOwner, P as FirstTreeHubSDK, R as applyClientLoggerConfig, 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-CNR-lUlr.mjs";
6
- import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-BOISS0DK.mjs";
5
+ import { A as stopPostgres, C as checkServerReachable, F as SdkError, I as SessionRegistry, L as cleanWorkspaces, M as createOwner, P as FirstTreeHubSDK, R as applyClientLoggerConfig, 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-VW2Qfs73.mjs";
6
+ import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-OezhDY7x.mjs";
7
7
  import { createRequire } from "node:module";
8
8
  import { Command } from "commander";
9
9
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
@@ -767,6 +767,90 @@ function registerAgentCommands(program) {
767
767
  }
768
768
  //#endregion
769
769
  //#region src/commands/connect.ts
770
+ /** Decode a JWT payload without signature verification. For UI purposes only. */
771
+ function decodeJwtPayload(token) {
772
+ try {
773
+ const parts = token.split(".");
774
+ if (parts.length !== 3 || !parts[1]) return null;
775
+ const raw = Buffer.from(parts[1], "base64url").toString();
776
+ const obj = JSON.parse(raw);
777
+ if (typeof obj !== "object" || obj === null) return null;
778
+ return obj;
779
+ } catch {
780
+ return null;
781
+ }
782
+ }
783
+ /**
784
+ * Detect if the current home already holds a setup for a *different* account,
785
+ * and give the operator a chance to back out before we overwrite credentials.
786
+ *
787
+ * Why this gate exists: running `client connect` implicitly overwrites
788
+ * `~/.first-tree-hub/config/credentials.json`. Without this prompt, someone
789
+ * onboarding a second account on their own machine silently logs themselves
790
+ * out of the first account — they'd only notice when their "main" agents
791
+ * appeared offline later. We treat single-account-per-machine as the product
792
+ * default; the `FIRST_TREE_HUB_HOME` env var remains the advanced escape
793
+ * hatch for power users who want parallel setups.
794
+ *
795
+ * The caller passes the *new* memberId directly so this gate can run BEFORE
796
+ * auth. That matters for the `--token` path: connect tokens are single-use;
797
+ * if we auth first and the user picks Cancel, the token is burned even
798
+ * though nothing changed on disk. Decoding the connect token locally lets
799
+ * us return early without spending it.
800
+ *
801
+ * Behavior:
802
+ * - No existing credentials → proceed silently (first-time install).
803
+ * - Existing credentials, same memberId → proceed silently (reconnect /
804
+ * token refresh — common + safe).
805
+ * - Existing credentials, memberId indeterminate → prompt with
806
+ * "unknown account" label so the user can decide.
807
+ * - Existing credentials, different memberId → prompt [Replace / Cancel].
808
+ * Cancel prints the isolation guide and returns "cancel".
809
+ */
810
+ async function promptReplaceOrCancel(newMemberId, newServerUrl) {
811
+ const existing = loadCredentials();
812
+ if (!existing) return "proceed";
813
+ const existingPayload = decodeJwtPayload(existing.accessToken);
814
+ const existingMemberId = typeof existingPayload?.memberId === "string" ? existingPayload.memberId : null;
815
+ if (existingMemberId && existingMemberId === newMemberId) return "proceed";
816
+ const existingMember = existingMemberId ? `member ${existingMemberId.slice(0, 8)}` : "unknown account";
817
+ const existingOrg = typeof existingPayload?.organizationId === "string" ? existingPayload.organizationId : null;
818
+ const serviceStatus = getClientServiceStatus();
819
+ const serviceLine = serviceStatus.state === "active" ? `running (${serviceStatus.detail ?? "live"})` : serviceStatus.state === "inactive" ? `installed but not running${serviceStatus.detail ? ` — ${serviceStatus.detail}` : ""}` : "not installed";
820
+ process.stderr.write("\n");
821
+ process.stderr.write(" ⚠️ This computer is already connected to the Hub under another account.\n\n");
822
+ process.stderr.write(` Existing account: ${existingMember}\n`);
823
+ if (existingOrg) process.stderr.write(` Organization: ${existingOrg.slice(0, 8)}\n`);
824
+ process.stderr.write(` Server: ${existing.serverUrl}\n`);
825
+ process.stderr.write(` Background service: ${serviceLine}\n\n`);
826
+ process.stderr.write(" Replacing only affects THIS computer. Your agents, messages, and\n");
827
+ process.stderr.write(" settings on the Hub itself are untouched.\n\n");
828
+ if (await select({
829
+ message: "How would you like to continue?",
830
+ choices: [{
831
+ name: "Replace — log out the other account and set up this one",
832
+ value: "replace"
833
+ }, {
834
+ name: "Cancel — keep the existing setup on this computer",
835
+ value: "cancel"
836
+ }]
837
+ }) === "cancel") {
838
+ printIsolationGuide(newServerUrl);
839
+ return "cancel";
840
+ }
841
+ return "proceed";
842
+ }
843
+ function printIsolationGuide(newServerUrl) {
844
+ process.stderr.write("\n Cancelled. The existing account on this computer is untouched.\n\n");
845
+ process.stderr.write(" To run this new account alongside it (advanced — no background service):\n\n");
846
+ process.stderr.write(" export FIRST_TREE_HUB_HOME=\"$HOME/.first-tree-hub-<label>\"\n");
847
+ process.stderr.write(` first-tree-hub client connect ${newServerUrl} --token <token>\n`);
848
+ process.stderr.write(" first-tree-hub client start\n\n");
849
+ process.stderr.write(" Notes:\n");
850
+ process.stderr.write(" - Run the commands in a FRESH terminal (the isolated home must be set first).\n");
851
+ process.stderr.write(" - In isolated mode the client stays online only while that terminal runs.\n");
852
+ process.stderr.write(" - The main account's background service is not affected.\n\n");
853
+ }
770
854
  /**
771
855
  * Authenticate via connect token — exchange for full JWT credentials.
772
856
  */
@@ -803,10 +887,27 @@ function registerConnectCommand(parent) {
803
887
  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) => {
804
888
  try {
805
889
  const url = serverUrl.replace(/\/+$/, "");
890
+ let preAuthDecided = false;
891
+ if (options.token) {
892
+ const connectPayload = decodeJwtPayload(options.token);
893
+ const newMemberId = typeof connectPayload?.memberId === "string" ? connectPayload.memberId : null;
894
+ if (newMemberId !== null) {
895
+ if (await promptReplaceOrCancel(newMemberId, url) === "cancel") return;
896
+ preAuthDecided = true;
897
+ }
898
+ }
899
+ const tokens = options.token ? await authenticateWithToken(url, options.token) : await authenticateInteractive(url);
900
+ if (!preAuthDecided) {
901
+ const newPayload = decodeJwtPayload(tokens.accessToken);
902
+ const newMemberId = typeof newPayload?.memberId === "string" ? newPayload.memberId : null;
903
+ if (newMemberId !== null) {
904
+ if (await promptReplaceOrCancel(newMemberId, url) === "cancel") return;
905
+ }
906
+ }
806
907
  setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", url);
807
908
  process.stderr.write(`\n \u2713 Server configured: ${url}\n`);
808
909
  saveCredentials({
809
- ...options.token ? await authenticateWithToken(url, options.token) : await authenticateInteractive(url),
910
+ ...tokens,
810
911
  serverUrl: url
811
912
  });
812
913
  process.stderr.write(" ✓ Authenticated\n");