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

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.
@@ -56,6 +56,10 @@ const clientConfigSchema = defineConfig({
56
56
  default: "http://localhost:8000"
57
57
  }
58
58
  }) },
59
+ client: { id: field(z.string().regex(/^client_[a-f0-9]{8}$/), {
60
+ auto: "client-id",
61
+ env: "FIRST_TREE_HUB_CLIENT_ID"
62
+ }) },
59
63
  logLevel: field(z.enum([
60
64
  "debug",
61
65
  "info",
@@ -112,6 +116,7 @@ function coerceEnvValue(value, schema) {
112
116
  return value;
113
117
  }
114
118
  function builtinAutoGenerate(strategy) {
119
+ if (strategy === "client-id") return `client_${randomBytes(4).toString("hex")}`;
115
120
  const match = /^random:(\w+):(\d+)$/.exec(strategy);
116
121
  if (!match) throw new Error(`Unknown auto-generation strategy: ${strategy}`);
117
122
  const encoding = match[1];
@@ -505,13 +510,13 @@ function resolveServerUrl(flagValue) {
505
510
  * Resolve the current member access JWT from persisted credentials.
506
511
  *
507
512
  * Unified-user-token milestone: the CLI has a single credential store and a
508
- * single onboarding path (`first-tree-hub connect`). The legacy
513
+ * single onboarding path (`first-tree-hub client connect`). The legacy
509
514
  * `FIRST_TREE_HUB_TOKEN` env var is no longer read — callers get a clear
510
- * error pointing at `connect` instead.
515
+ * error pointing at `client connect` instead.
511
516
  */
512
517
  function resolveAccessToken() {
513
518
  const creds = loadCredentials();
514
- if (!creds) throw new Error("No credentials found. Run `first-tree-hub connect <server-url>` to sign in.");
519
+ if (!creds) throw new Error("No credentials found. Run `first-tree-hub client connect <server-url>` to sign in.");
515
520
  return creds.accessToken;
516
521
  }
517
522
  /**
@@ -521,7 +526,7 @@ function resolveAccessToken() {
521
526
  */
522
527
  async function ensureFreshAccessToken() {
523
528
  const creds = loadCredentials();
524
- if (!creds) throw new Error("No credentials found. Run `first-tree-hub connect <server-url>` to sign in.");
529
+ if (!creds) throw new Error("No credentials found. Run `first-tree-hub client connect <server-url>` to sign in.");
525
530
  if (!isTokenExpired(creds.accessToken)) return creds.accessToken;
526
531
  const res = await fetch(`${creds.serverUrl}/api/v1/auth/refresh`, {
527
532
  method: "POST",
@@ -529,7 +534,7 @@ async function ensureFreshAccessToken() {
529
534
  body: JSON.stringify({ refreshToken: creds.refreshToken }),
530
535
  signal: AbortSignal.timeout(1e4)
531
536
  });
532
- if (!res.ok) throw new Error("Access token expired and refresh failed. Run `first-tree-hub connect <server-url>`.");
537
+ if (!res.ok) throw new Error("Access token expired and refresh failed. Run `first-tree-hub client connect <server-url>`.");
533
538
  const data = await res.json();
534
539
  saveCredentials({
535
540
  ...creds,
@@ -545,7 +550,7 @@ function isTokenExpired(token) {
545
550
  if (parts.length !== 3 || !parts[1]) return true;
546
551
  const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
547
552
  if (!payload.exp) return false;
548
- return payload.exp * 1e3 < Date.now() - 3e4;
553
+ return payload.exp * 1e3 < Date.now() + 3e4;
549
554
  } catch {
550
555
  return true;
551
556
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
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-DW7aIpmE.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-RXUUKkCO.mjs";
4
- import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-BZ8pnMrQ.mjs";
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";
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";
7
7
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
@@ -362,7 +362,7 @@ function registerAgentCommands(program) {
362
362
  process.stderr.write(" No agents configured.\n");
363
363
  }
364
364
  });
365
- agent.command("create <name>").description("Create an agent on Hub and bind it locally").requiredOption("--type <type>", "Agent type (human, personal_assistant, autonomous_agent)").requiredOption("--client-id <id>", "Client (machine) that will run this agent — must be owned by you. Run `first-tree-hub connect` on that machine first.").option("--runtime <runtime>", "Runtime handler (default: claude-code)", "claude-code").option("--display-name <name>", "Display name").option("--server <url>", "Hub server URL").action(async (name, options) => {
365
+ agent.command("create <name>").description("Create an agent on Hub and bind it locally").requiredOption("--type <type>", "Agent type (human, personal_assistant, autonomous_agent)").requiredOption("--client-id <id>", "Client (machine) that will run this agent — must be owned by you. Run `first-tree-hub client connect` on that machine first.").option("--runtime <runtime>", "Runtime handler (default: claude-code)", "claude-code").option("--display-name <name>", "Display name").option("--server <url>", "Hub server URL").action(async (name, options) => {
366
366
  try {
367
367
  const serverUrl = resolveServerUrl(options.server);
368
368
  const headers = {
@@ -439,7 +439,31 @@ function registerAgentCommands(program) {
439
439
  }
440
440
  process.stderr.write(` ${totalRemoved} workspace(s) cleaned.\n`);
441
441
  });
442
- const bind = agent.command("bind").description("Bind external IM accounts to agents");
442
+ const bind = agent.command("bind").description("Bind an agent to a client machine or external IM account");
443
+ bind.command("client <agentName>").description("Bind an unbound agent to a client machine (first-time bind only — ID is immutable once set)").requiredOption("--client-id <id>", "Client (machine) ID — must be owned by you. Run `first-tree-hub client connect` on that machine first.").option("--server <url>", "Hub server URL").action(async (agentName, options) => {
444
+ try {
445
+ const serverUrl = resolveServerUrl(options.server);
446
+ const accessToken = await ensureFreshAccessToken();
447
+ const target = await resolveAgent(serverUrl, accessToken, agentName);
448
+ const patchRes = await fetch(`${serverUrl}/api/v1/admin/agents/${target.uuid}`, {
449
+ method: "PATCH",
450
+ headers: {
451
+ Authorization: `Bearer ${accessToken}`,
452
+ "Content-Type": "application/json"
453
+ },
454
+ body: JSON.stringify({ clientId: options.clientId }),
455
+ signal: AbortSignal.timeout(1e4)
456
+ });
457
+ if (!patchRes.ok) fail("BIND_CLIENT_ERROR", (await patchRes.json().catch(() => ({}))).error ?? `Bind failed (HTTP ${patchRes.status})`, 1);
458
+ process.stderr.write(` \u2713 Bound "${target.name ?? target.uuid}" to client ${options.clientId}.\n`);
459
+ success({
460
+ agentId: target.uuid,
461
+ clientId: options.clientId
462
+ });
463
+ } catch (error) {
464
+ fail("BIND_CLIENT_ERROR", error instanceof Error ? error.message : String(error));
465
+ }
466
+ });
443
467
  bind.command("bot").description("Bind a Feishu bot to this agent (self-service)").requiredOption("--platform <platform>", "Platform: feishu").requiredOption("--app-id <id>", "Feishu bot App ID").requiredOption("--app-secret <secret>", "Feishu bot App Secret").option("--agent <name>", "Local agent alias (default: first configured)").option("--server <url>", "Hub server URL").action(async (options) => {
444
468
  try {
445
469
  if (options.platform !== "feishu") fail("UNSUPPORTED_PLATFORM", `Platform "${options.platform}" is not supported. Use "feishu".`);
@@ -740,9 +764,94 @@ function registerAgentCommands(program) {
740
764
  });
741
765
  }
742
766
  //#endregion
767
+ //#region src/commands/connect.ts
768
+ /**
769
+ * Authenticate via connect token — exchange for full JWT credentials.
770
+ */
771
+ async function authenticateWithToken(url, token) {
772
+ const res = await fetch(`${url}/api/v1/auth/connect-token`, {
773
+ method: "POST",
774
+ headers: { "Content-Type": "application/json" },
775
+ body: JSON.stringify({ token }),
776
+ signal: AbortSignal.timeout(1e4)
777
+ });
778
+ if (!res.ok) fail("AUTH_ERROR", (await res.json().catch(() => ({}))).error ?? `Token exchange failed (HTTP ${res.status})`, 1);
779
+ return await res.json();
780
+ }
781
+ /**
782
+ * Authenticate via interactive username/password login.
783
+ */
784
+ async function authenticateInteractive(url) {
785
+ process.stderr.write("\n Log in to Hub:\n");
786
+ const username = await input({ message: " Username:" });
787
+ const pw = await password({ message: " Password:" });
788
+ const loginRes = await fetch(`${url}/api/v1/auth/login`, {
789
+ method: "POST",
790
+ headers: { "Content-Type": "application/json" },
791
+ body: JSON.stringify({
792
+ username,
793
+ password: pw
794
+ }),
795
+ signal: AbortSignal.timeout(1e4)
796
+ });
797
+ if (!loginRes.ok) fail("AUTH_ERROR", (await loginRes.json().catch(() => ({}))).error ?? `Login failed (HTTP ${loginRes.status})`, 1);
798
+ return await loginRes.json();
799
+ }
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) => {
802
+ try {
803
+ const url = serverUrl.replace(/\/+$/, "");
804
+ setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", url);
805
+ process.stderr.write(`\n \u2713 Server configured: ${url}\n`);
806
+ saveCredentials({
807
+ ...options.token ? await authenticateWithToken(url, options.token) : await authenticateInteractive(url),
808
+ serverUrl: url
809
+ });
810
+ process.stderr.write(" ✓ Authenticated\n");
811
+ resetConfig();
812
+ resetConfigMeta();
813
+ const config = await initConfig({
814
+ schema: clientConfigSchema,
815
+ role: "client"
816
+ });
817
+ const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
818
+ const agents = loadAgents({
819
+ schema: agentConfigSchema,
820
+ agentsDir
821
+ });
822
+ process.stderr.write(`\n Starting client (id: ${config.client.id})...\n`);
823
+ const runtime = new ClientRuntime(config.server.url, config.client.id);
824
+ for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
825
+ await runtime.start();
826
+ runtime.watchAgentsDir(agentsDir);
827
+ const shutdown = async () => {
828
+ process.stderr.write("\n Shutting down...\n");
829
+ runtime.unwatchAgentsDir();
830
+ await runtime.stop();
831
+ process.exit(0);
832
+ };
833
+ process.on("SIGINT", () => void shutdown());
834
+ process.on("SIGTERM", () => void shutdown());
835
+ await new Promise(() => {});
836
+ } catch (error) {
837
+ if (error.name === "ExitPromptError") {
838
+ process.stderr.write("\n Cancelled.\n");
839
+ return;
840
+ }
841
+ const msg = error instanceof Error ? error.message : String(error);
842
+ process.stderr.write(` Error: ${msg}\n`);
843
+ process.exit(1);
844
+ } finally {
845
+ resetConfig();
846
+ resetConfigMeta();
847
+ }
848
+ });
849
+ }
850
+ //#endregion
743
851
  //#region src/commands/client.ts
744
852
  function registerClientCommands(program) {
745
853
  const client = program.command("client").description("Client runtime — connect agents to the server");
854
+ registerConnectCommand(client);
746
855
  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) => {
747
856
  try {
748
857
  await promptMissingFields({
@@ -759,8 +868,8 @@ function registerClientCommands(program) {
759
868
  schema: agentConfigSchema,
760
869
  agentsDir
761
870
  });
762
- process.stderr.write(`\n Connecting to ${config.server.url}...\n`);
763
- const runtime = new ClientRuntime(config.server.url);
871
+ process.stderr.write(`\n Connecting to ${config.server.url} (client id: ${config.client.id})...\n`);
872
+ const runtime = new ClientRuntime(config.server.url, config.client.id);
764
873
  for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
765
874
  await runtime.start();
766
875
  runtime.watchAgentsDir(agentsDir);
@@ -814,21 +923,21 @@ function registerClientCommands(program) {
814
923
  process.stderr.write(" No agents directory found.\n");
815
924
  }
816
925
  });
817
- client.command("hub-list").description("List connected clients on the Hub server").option("--server <url>", "Hub server URL").action(async (options) => {
926
+ client.command("hub-list").description("List clients on the Hub server").option("--server <url>", "Hub server URL").action(async (options) => {
818
927
  try {
819
928
  const serverUrl = resolveServerUrl(options.server);
820
929
  const token = await ensureFreshAccessToken();
821
- const response = await fetch(`${serverUrl}/api/v1/admin/clients`, {
930
+ const response = await fetch(`${serverUrl}/api/v1/clients`, {
822
931
  headers: { Authorization: `Bearer ${token}` },
823
932
  signal: AbortSignal.timeout(1e4)
824
933
  });
825
934
  if (!response.ok) fail("FETCH_ERROR", `Server returned ${response.status}`, 1);
826
935
  const clients = await response.json();
827
936
  if (clients.length === 0) {
828
- process.stderr.write(" No connected clients.\n");
937
+ process.stderr.write(" No clients.\n");
829
938
  return;
830
939
  }
831
- process.stderr.write(`\n Connected Clients: ${clients.length}\n\n`);
940
+ process.stderr.write(`\n Clients: ${clients.length}\n\n`);
832
941
  const header = ` ${"CLIENT".padEnd(20)} ${"HOST".padEnd(25)} ${"AGENTS".padEnd(8)} CONNECTED`;
833
942
  process.stderr.write(`${header}\n`);
834
943
  process.stderr.write(` ${"─".repeat(header.length - 2)}\n`);
@@ -845,7 +954,7 @@ function registerClientCommands(program) {
845
954
  try {
846
955
  const serverUrl = resolveServerUrl(options.server);
847
956
  const token = await ensureFreshAccessToken();
848
- const response = await fetch(`${serverUrl}/api/v1/admin/clients/${clientId}/disconnect`, {
957
+ const response = await fetch(`${serverUrl}/api/v1/clients/${clientId}/disconnect`, {
849
958
  method: "POST",
850
959
  headers: { Authorization: `Bearer ${token}` },
851
960
  signal: AbortSignal.timeout(1e4)
@@ -961,101 +1070,17 @@ function isSecretField(schema, dotPath) {
961
1070
  return false;
962
1071
  }
963
1072
  //#endregion
964
- //#region src/commands/connect.ts
965
- /**
966
- * Authenticate via connect token — exchange for full JWT credentials.
967
- */
968
- async function authenticateWithToken(url, token) {
969
- const res = await fetch(`${url}/api/v1/auth/connect-token`, {
970
- method: "POST",
971
- headers: { "Content-Type": "application/json" },
972
- body: JSON.stringify({ token }),
973
- signal: AbortSignal.timeout(1e4)
974
- });
975
- if (!res.ok) fail("AUTH_ERROR", (await res.json().catch(() => ({}))).error ?? `Token exchange failed (HTTP ${res.status})`, 1);
976
- return await res.json();
977
- }
978
- /**
979
- * Authenticate via interactive username/password login.
980
- */
981
- async function authenticateInteractive(url) {
982
- process.stderr.write("\n Log in to Hub:\n");
983
- const username = await input({ message: " Username:" });
984
- const pw = await password({ message: " Password:" });
985
- const loginRes = await fetch(`${url}/api/v1/auth/login`, {
986
- method: "POST",
987
- headers: { "Content-Type": "application/json" },
988
- body: JSON.stringify({
989
- username,
990
- password: pw
991
- }),
992
- signal: AbortSignal.timeout(1e4)
993
- });
994
- if (!loginRes.ok) fail("AUTH_ERROR", (await loginRes.json().catch(() => ({}))).error ?? `Login failed (HTTP ${loginRes.status})`, 1);
995
- return await loginRes.json();
996
- }
997
- function registerConnectCommand(program) {
998
- program.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) => {
999
- try {
1000
- const url = serverUrl.replace(/\/+$/, "");
1001
- setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", url);
1002
- process.stderr.write(`\n \u2713 Server configured: ${url}\n`);
1003
- saveCredentials({
1004
- ...options.token ? await authenticateWithToken(url, options.token) : await authenticateInteractive(url),
1005
- serverUrl: url
1006
- });
1007
- process.stderr.write(" ✓ Authenticated\n");
1008
- resetConfig();
1009
- resetConfigMeta();
1010
- const config = await initConfig({
1011
- schema: clientConfigSchema,
1012
- role: "client"
1013
- });
1014
- const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
1015
- const agents = loadAgents({
1016
- schema: agentConfigSchema,
1017
- agentsDir
1018
- });
1019
- process.stderr.write(`\n Starting client...\n`);
1020
- const runtime = new ClientRuntime(config.server.url);
1021
- for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
1022
- await runtime.start();
1023
- runtime.watchAgentsDir(agentsDir);
1024
- const shutdown = async () => {
1025
- process.stderr.write("\n Shutting down...\n");
1026
- runtime.unwatchAgentsDir();
1027
- await runtime.stop();
1028
- process.exit(0);
1029
- };
1030
- process.on("SIGINT", () => void shutdown());
1031
- process.on("SIGTERM", () => void shutdown());
1032
- await new Promise(() => {});
1033
- } catch (error) {
1034
- if (error.name === "ExitPromptError") {
1035
- process.stderr.write("\n Cancelled.\n");
1036
- return;
1037
- }
1038
- const msg = error instanceof Error ? error.message : String(error);
1039
- process.stderr.write(` Error: ${msg}\n`);
1040
- process.exit(1);
1041
- } finally {
1042
- resetConfig();
1043
- resetConfigMeta();
1044
- }
1045
- });
1046
- }
1047
- //#endregion
1048
1073
  //#region src/commands/onboard.ts
1049
1074
  async function promptMissing(args) {
1050
1075
  if (!args.server) try {
1051
- const { resolveServerUrl } = await import("../bootstrap-DW7aIpmE.mjs").then((n) => n.t);
1076
+ const { resolveServerUrl } = await import("../bootstrap-CRDR6NwE.mjs").then((n) => n.t);
1052
1077
  resolveServerUrl();
1053
1078
  } catch {
1054
1079
  args.server = await input({ message: "Hub server URL:" });
1055
1080
  saveOnboardState(args);
1056
1081
  }
1057
- const { loadCredentials } = await import("../bootstrap-DW7aIpmE.mjs").then((n) => n.t);
1058
- if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub connect <server-url>` before onboarding.");
1082
+ const { loadCredentials } = await import("../bootstrap-CRDR6NwE.mjs").then((n) => n.t);
1083
+ if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub client connect <server-url>` before onboarding.");
1059
1084
  if (!args.id) {
1060
1085
  args.id = await input({ message: "Agent ID:" });
1061
1086
  saveOnboardState(args);
@@ -1080,11 +1105,9 @@ async function promptMissing(args) {
1080
1105
  });
1081
1106
  saveOnboardState(args);
1082
1107
  }
1083
- if (args.type !== "human" && !args.clientId) {
1084
- args.clientId = await input({
1085
- message: "Client ID (machine that will run this agent — must be owned by you):",
1086
- validate: (v) => v.length > 0 ? true : "clientId is required for non-human agents"
1087
- });
1108
+ if (args.type !== "human" && args.clientId === void 0) {
1109
+ args.clientId = await input({ message: "Client ID (Enter to leave unbound — first WS connect will claim it):" });
1110
+ if (!args.clientId) args.clientId = void 0;
1088
1111
  saveOnboardState(args);
1089
1112
  }
1090
1113
  if (!args.role) {
@@ -1117,10 +1140,7 @@ async function promptMissing(args) {
1117
1140
  message: "Assistant ID:",
1118
1141
  default: `${args.id}-assistant`
1119
1142
  });
1120
- if (!args.clientId) args.clientId = await input({
1121
- message: "Client ID for the assistant (must be owned by you):",
1122
- validate: (v) => v.length > 0 ? true : "clientId is required"
1123
- });
1143
+ if (args.clientId === void 0) args.clientId = await input({ message: "Client ID for the assistant (Enter to leave unbound):" }) || void 0;
1124
1144
  saveOnboardState(args);
1125
1145
  }
1126
1146
  }
@@ -1327,7 +1347,6 @@ registerClientCommands(program);
1327
1347
  registerAgentCommands(program);
1328
1348
  registerConfigCommands(program);
1329
1349
  registerStatusCommand(program);
1330
- registerConnectCommand(program);
1331
1350
  registerOnboardCommand(program);
1332
1351
  program.parse();
1333
1352
  //#endregion