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

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,
@@ -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-DNL1cEwv.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-B10jgThe.mjs";
4
+ import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-BoMJHlOv.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 = {
@@ -740,9 +740,94 @@ function registerAgentCommands(program) {
740
740
  });
741
741
  }
742
742
  //#endregion
743
+ //#region src/commands/connect.ts
744
+ /**
745
+ * Authenticate via connect token — exchange for full JWT credentials.
746
+ */
747
+ async function authenticateWithToken(url, token) {
748
+ const res = await fetch(`${url}/api/v1/auth/connect-token`, {
749
+ method: "POST",
750
+ headers: { "Content-Type": "application/json" },
751
+ body: JSON.stringify({ token }),
752
+ signal: AbortSignal.timeout(1e4)
753
+ });
754
+ if (!res.ok) fail("AUTH_ERROR", (await res.json().catch(() => ({}))).error ?? `Token exchange failed (HTTP ${res.status})`, 1);
755
+ return await res.json();
756
+ }
757
+ /**
758
+ * Authenticate via interactive username/password login.
759
+ */
760
+ async function authenticateInteractive(url) {
761
+ process.stderr.write("\n Log in to Hub:\n");
762
+ const username = await input({ message: " Username:" });
763
+ const pw = await password({ message: " Password:" });
764
+ const loginRes = await fetch(`${url}/api/v1/auth/login`, {
765
+ method: "POST",
766
+ headers: { "Content-Type": "application/json" },
767
+ body: JSON.stringify({
768
+ username,
769
+ password: pw
770
+ }),
771
+ signal: AbortSignal.timeout(1e4)
772
+ });
773
+ if (!loginRes.ok) fail("AUTH_ERROR", (await loginRes.json().catch(() => ({}))).error ?? `Login failed (HTTP ${loginRes.status})`, 1);
774
+ return await loginRes.json();
775
+ }
776
+ function registerConnectCommand(parent) {
777
+ 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) => {
778
+ try {
779
+ const url = serverUrl.replace(/\/+$/, "");
780
+ setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", url);
781
+ process.stderr.write(`\n \u2713 Server configured: ${url}\n`);
782
+ saveCredentials({
783
+ ...options.token ? await authenticateWithToken(url, options.token) : await authenticateInteractive(url),
784
+ serverUrl: url
785
+ });
786
+ process.stderr.write(" ✓ Authenticated\n");
787
+ resetConfig();
788
+ resetConfigMeta();
789
+ const config = await initConfig({
790
+ schema: clientConfigSchema,
791
+ role: "client"
792
+ });
793
+ const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
794
+ const agents = loadAgents({
795
+ schema: agentConfigSchema,
796
+ agentsDir
797
+ });
798
+ process.stderr.write(`\n Starting client (id: ${config.client.id})...\n`);
799
+ const runtime = new ClientRuntime(config.server.url, config.client.id);
800
+ for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
801
+ await runtime.start();
802
+ runtime.watchAgentsDir(agentsDir);
803
+ const shutdown = async () => {
804
+ process.stderr.write("\n Shutting down...\n");
805
+ runtime.unwatchAgentsDir();
806
+ await runtime.stop();
807
+ process.exit(0);
808
+ };
809
+ process.on("SIGINT", () => void shutdown());
810
+ process.on("SIGTERM", () => void shutdown());
811
+ await new Promise(() => {});
812
+ } catch (error) {
813
+ if (error.name === "ExitPromptError") {
814
+ process.stderr.write("\n Cancelled.\n");
815
+ return;
816
+ }
817
+ const msg = error instanceof Error ? error.message : String(error);
818
+ process.stderr.write(` Error: ${msg}\n`);
819
+ process.exit(1);
820
+ } finally {
821
+ resetConfig();
822
+ resetConfigMeta();
823
+ }
824
+ });
825
+ }
826
+ //#endregion
743
827
  //#region src/commands/client.ts
744
828
  function registerClientCommands(program) {
745
829
  const client = program.command("client").description("Client runtime — connect agents to the server");
830
+ registerConnectCommand(client);
746
831
  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
832
  try {
748
833
  await promptMissingFields({
@@ -759,8 +844,8 @@ function registerClientCommands(program) {
759
844
  schema: agentConfigSchema,
760
845
  agentsDir
761
846
  });
762
- process.stderr.write(`\n Connecting to ${config.server.url}...\n`);
763
- const runtime = new ClientRuntime(config.server.url);
847
+ process.stderr.write(`\n Connecting to ${config.server.url} (client id: ${config.client.id})...\n`);
848
+ const runtime = new ClientRuntime(config.server.url, config.client.id);
764
849
  for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
765
850
  await runtime.start();
766
851
  runtime.watchAgentsDir(agentsDir);
@@ -961,101 +1046,17 @@ function isSecretField(schema, dotPath) {
961
1046
  return false;
962
1047
  }
963
1048
  //#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
1049
  //#region src/commands/onboard.ts
1049
1050
  async function promptMissing(args) {
1050
1051
  if (!args.server) try {
1051
- const { resolveServerUrl } = await import("../bootstrap-DW7aIpmE.mjs").then((n) => n.t);
1052
+ const { resolveServerUrl } = await import("../bootstrap-DNL1cEwv.mjs").then((n) => n.t);
1052
1053
  resolveServerUrl();
1053
1054
  } catch {
1054
1055
  args.server = await input({ message: "Hub server URL:" });
1055
1056
  saveOnboardState(args);
1056
1057
  }
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.");
1058
+ const { loadCredentials } = await import("../bootstrap-DNL1cEwv.mjs").then((n) => n.t);
1059
+ if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub client connect <server-url>` before onboarding.");
1059
1060
  if (!args.id) {
1060
1061
  args.id = await input({ message: "Agent ID:" });
1061
1062
  saveOnboardState(args);
@@ -1080,11 +1081,9 @@ async function promptMissing(args) {
1080
1081
  });
1081
1082
  saveOnboardState(args);
1082
1083
  }
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
- });
1084
+ if (args.type !== "human" && args.clientId === void 0) {
1085
+ args.clientId = await input({ message: "Client ID (Enter to leave unbound — first WS connect will claim it):" });
1086
+ if (!args.clientId) args.clientId = void 0;
1088
1087
  saveOnboardState(args);
1089
1088
  }
1090
1089
  if (!args.role) {
@@ -1117,10 +1116,7 @@ async function promptMissing(args) {
1117
1116
  message: "Assistant ID:",
1118
1117
  default: `${args.id}-assistant`
1119
1118
  });
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
- });
1119
+ if (args.clientId === void 0) args.clientId = await input({ message: "Client ID for the assistant (Enter to leave unbound):" }) || void 0;
1124
1120
  saveOnboardState(args);
1125
1121
  }
1126
1122
  }
@@ -1327,7 +1323,6 @@ registerClientCommands(program);
1327
1323
  registerAgentCommands(program);
1328
1324
  registerConfigCommands(program);
1329
1325
  registerStatusCommand(program);
1330
- registerConnectCommand(program);
1331
1326
  registerOnboardCommand(program);
1332
1327
  program.parse();
1333
1328
  //#endregion
@@ -1,5 +1,5 @@
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-DW7aIpmE.mjs";
2
- import { $ as updateOrganizationSchema, 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 taskListQuerySchema, K as sessionOutputMessageSchema, L as loginSchema, M as delegateFeishuUserSchema, N as dryRunAgentRuntimeConfigSchema, O as createChatSchema, P as inboxPollQuerySchema, Q as updateMemberSchema, R as messageSourceSchema$1, S as agentTypeSchema$1, T as createAdapterConfigSchema, U as selfServiceFeishuBotSchema, V as refreshTokenSchema, W as sendMessageSchema, X as updateAgentRuntimeConfigSchema, Y as updateAdapterConfigSchema, Z as updateAgentSchema, _ as addParticipantSchema, a as AGENT_SELECTOR_HEADER$1, b as agentBindRequestSchema, c as AGENT_TYPES, d as SYSTEM_CONFIG_DEFAULTS, et as updateSystemConfigSchema, f as TASK_CREATOR_TYPES, g as WS_AUTH_FRAME_TIMEOUT_MS, h as TASK_TERMINAL_STATUSES, i as AGENT_BIND_REJECT_REASONS, j as createTaskSchema, k as createMemberSchema, l as AGENT_VISIBILITY, m as TASK_STATUSES, nt as wsAuthFrameSchema, o as AGENT_SOURCES, p as TASK_HEALTH_SIGNALS, q as sessionStateMessageSchema, s as AGENT_STATUSES, tt as updateTaskStatusSchema, 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-BZ8pnMrQ.mjs";
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-DNL1cEwv.mjs";
2
+ import { $ as updateOrganizationSchema, 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 taskListQuerySchema, K as sessionOutputMessageSchema, L as loginSchema, M as delegateFeishuUserSchema, N as dryRunAgentRuntimeConfigSchema, O as createChatSchema, P as inboxPollQuerySchema, Q as updateMemberSchema, R as messageSourceSchema$1, S as agentTypeSchema$1, T as createAdapterConfigSchema, U as selfServiceFeishuBotSchema, V as refreshTokenSchema, W as sendMessageSchema, X as updateAgentRuntimeConfigSchema, Y as updateAdapterConfigSchema, Z as updateAgentSchema, _ as addParticipantSchema, a as AGENT_SELECTOR_HEADER$1, b as agentBindRequestSchema, c as AGENT_TYPES, d as SYSTEM_CONFIG_DEFAULTS, et as updateSystemConfigSchema, f as TASK_CREATOR_TYPES, g as WS_AUTH_FRAME_TIMEOUT_MS, h as TASK_TERMINAL_STATUSES, i as AGENT_BIND_REJECT_REASONS, j as createTaskSchema, k as createMemberSchema, l as AGENT_VISIBILITY, m as TASK_STATUSES, nt as wsAuthFrameSchema, o as AGENT_SOURCES, p as TASK_HEALTH_SIGNALS, q as sessionStateMessageSchema, s as AGENT_STATUSES, tt as updateTaskStatusSchema, 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-BoMJHlOv.mjs";
3
3
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { ZodError, z } from "zod";
@@ -11,7 +11,7 @@ import WebSocket from "ws";
11
11
  import { query } from "@anthropic-ai/claude-agent-sdk";
12
12
  import { execFileSync, execSync, spawn } from "node:child_process";
13
13
  import bcrypt from "bcrypt";
14
- import { and, asc, count, desc, eq, gt, inArray, isNotNull, lt, ne, or, sql } from "drizzle-orm";
14
+ import { and, asc, count, desc, eq, gt, inArray, isNotNull, isNull, lt, ne, or, sql } from "drizzle-orm";
15
15
  import { drizzle } from "drizzle-orm/postgres-js";
16
16
  import postgres from "postgres";
17
17
  import { fileURLToPath } from "node:url";
@@ -172,7 +172,7 @@ z.object({
172
172
  visibility: agentVisibilitySchema.optional(),
173
173
  metadata: z.record(z.string(), z.unknown()).optional(),
174
174
  managerId: z.string().nullable().optional(),
175
- clientId: z.string().optional()
175
+ clientId: z.string().min(1).max(100).nullable().optional()
176
176
  });
177
177
  z.object({
178
178
  uuid: z.string(),
@@ -1277,6 +1277,10 @@ defineConfig({
1277
1277
  default: "http://localhost:8000"
1278
1278
  }
1279
1279
  }) },
1280
+ client: { id: field(z.string().regex(/^client_[a-f0-9]{8}$/), {
1281
+ auto: "client-id",
1282
+ env: "FIRST_TREE_HUB_CLIENT_ID"
1283
+ }) },
1280
1284
  logLevel: field(z.enum([
1281
1285
  "debug",
1282
1286
  "info",
@@ -3036,10 +3040,11 @@ var ClientRuntime = class {
3036
3040
  agentNames = /* @__PURE__ */ new Set();
3037
3041
  watcher = null;
3038
3042
  debounceTimer = null;
3039
- constructor(serverUrl) {
3043
+ constructor(serverUrl, clientId) {
3040
3044
  this.serverUrl = serverUrl;
3041
3045
  this.connection = new ClientConnection({
3042
3046
  serverUrl,
3047
+ clientId,
3043
3048
  getAccessToken: () => ensureFreshAccessToken()
3044
3049
  });
3045
3050
  registerBuiltinHandlers();
@@ -3655,7 +3660,7 @@ async function onboardCheck(args) {
3655
3660
  key: "connect",
3656
3661
  label: "Signed in",
3657
3662
  status: "missing_required",
3658
- hint: "Run `first-tree-hub connect <server-url>` first"
3663
+ hint: "Run `first-tree-hub client connect <server-url>` first"
3659
3664
  });
3660
3665
  try {
3661
3666
  const serverUrl = resolveServerUrl(args.server);
@@ -3713,11 +3718,17 @@ async function onboardCheck(args) {
3713
3718
  status: "missing_required",
3714
3719
  hint: "Provide via --type"
3715
3720
  });
3716
- if (args.type && args.type !== "human" && !args.clientId) items.push({
3721
+ if (args.type && args.type !== "human") if (args.clientId) items.push({
3717
3722
  key: "client",
3718
3723
  label: "Target client",
3719
- status: "missing_required",
3720
- hint: "Non-human agents must pin a client via --client-id <id>"
3724
+ status: "ok",
3725
+ value: args.clientId
3726
+ });
3727
+ else items.push({
3728
+ key: "client",
3729
+ label: "Target client",
3730
+ status: "ok",
3731
+ value: "(unbound — claimed on first WS connect)"
3721
3732
  });
3722
3733
  return items;
3723
3734
  }
@@ -3788,7 +3799,7 @@ async function onboardCreate(args) {
3788
3799
  }
3789
3800
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
3790
3801
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
3791
- const { bindFeishuBot } = await import("./feishu-BZ8pnMrQ.mjs").then((n) => n.r);
3802
+ const { bindFeishuBot } = await import("./feishu-BoMJHlOv.mjs").then((n) => n.r);
3792
3803
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
3793
3804
  if (!targetAgentUuid) process.stderr.write(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
3794
3805
  else {
@@ -3929,7 +3940,7 @@ function setNestedByDot(obj, dotPath, value) {
3929
3940
  if (lastKey !== void 0) current[lastKey] = value;
3930
3941
  }
3931
3942
  //#endregion
3932
- //#region ../server/dist/app-C7MR8S4r.mjs
3943
+ //#region ../server/dist/app-BjIHPMKY.mjs
3933
3944
  var __defProp = Object.defineProperty;
3934
3945
  var __exportAll = (all, no_symbols) => {
3935
3946
  let target = {};
@@ -4852,18 +4863,20 @@ function defaultVisibility(type) {
4852
4863
  /**
4853
4864
  * Resolve + validate the client that will own the new agent.
4854
4865
  *
4855
- * Rule (unified-user-token M1):
4856
- * - Non-human agents MUST pin a client at creation time. The pinned client
4857
- * must belong to the manager's user (Rule R-RUN upstream).
4858
- * - Human agents represent the member themselves and have no runtime, so a
4859
- * missing `clientId` is allowed and the column stays NULL.
4866
+ * Rule (unified-user-token, post-first-bind relaxation):
4867
+ * - Human agents represent the member themselves and have no runtime; a
4868
+ * missing `clientId` is required and the column stays NULL.
4869
+ * - Non-human agents MAY omit `clientId` at creation; the row stays NULL
4870
+ * and is claimed on the first WS bind (see `api/agent/ws-client.ts`).
4871
+ * - When a non-human agent IS created with a `clientId`, the pinned client
4872
+ * must already be owned by the manager's user (Rule R-RUN).
4860
4873
  */
4861
4874
  async function resolveAgentClient(db, data) {
4862
4875
  if (data.type === "human") {
4863
4876
  if (data.clientId) throw new BadRequestError("Human agents cannot be pinned to a client");
4864
4877
  return null;
4865
4878
  }
4866
- if (!data.clientId) throw new BadRequestError("clientId is required — every non-human agent must be pinned to a client at creation time. Run `first-tree-hub connect` on the target machine first.");
4879
+ if (!data.clientId) return null;
4867
4880
  const [manager] = await db.select({ userId: members.userId }).from(members).where(eq(members.id, data.managerId)).limit(1);
4868
4881
  if (!manager) throw new BadRequestError(`Manager "${data.managerId}" not found`);
4869
4882
  const [client] = await db.select({
@@ -4871,7 +4884,7 @@ async function resolveAgentClient(db, data) {
4871
4884
  userId: clients.userId
4872
4885
  }).from(clients).where(eq(clients.id, data.clientId)).limit(1);
4873
4886
  if (!client) throw new BadRequestError(`Client "${data.clientId}" not found`);
4874
- if (!client.userId) throw new BadRequestError(`Client "${data.clientId}" has not been claimed by a user yet. Have the operator run \`first-tree-hub connect\` on that machine before pinning an agent to it.`);
4887
+ if (!client.userId) throw new BadRequestError(`Client "${data.clientId}" has not been claimed by a user yet. Have the operator run \`first-tree-hub client connect\` on that machine before pinning an agent to it.`);
4875
4888
  if (client.userId !== manager.userId) throw new ForbiddenError(`Client "${data.clientId}" is not owned by the manager's user — pick a client belonging to that user.`);
4876
4889
  return client.id;
4877
4890
  }
@@ -4973,8 +4986,11 @@ async function listAgentsForMember(db, scope, limit, cursor, type) {
4973
4986
  };
4974
4987
  }
4975
4988
  async function updateAgent(db, uuid, data) {
4976
- if (data.clientId !== void 0) throw new BadRequestError("clientId is immutable in this milestone — delete and re-create the agent on the target client to move it");
4977
4989
  const agent = await getAgent(db, uuid);
4990
+ if (data.clientId !== void 0) {
4991
+ if (data.clientId === null) throw new BadRequestError("clientId cannot be cleared — once bound, an agent stays bound to its client");
4992
+ if (agent.clientId !== null && agent.clientId !== data.clientId) throw new BadRequestError("clientId is immutable once set — delete and re-create the agent on the target client to move it");
4993
+ }
4978
4994
  const updates = { updatedAt: /* @__PURE__ */ new Date() };
4979
4995
  if (data.type !== void 0) updates.type = data.type;
4980
4996
  if (data.displayName !== void 0) updates.displayName = data.displayName;
@@ -4991,6 +5007,14 @@ async function updateAgent(db, uuid, data) {
4991
5007
  if (manager.organizationId !== agent.organizationId) throw new BadRequestError("Manager must belong to the same organization as the agent");
4992
5008
  updates.managerId = data.managerId;
4993
5009
  }
5010
+ if (data.clientId !== void 0 && data.clientId !== null && agent.clientId === null) {
5011
+ const resolvedClientId = await resolveAgentClient(db, {
5012
+ clientId: data.clientId,
5013
+ managerId: updates.managerId ?? agent.managerId,
5014
+ type: agent.type
5015
+ });
5016
+ if (resolvedClientId !== null) updates.clientId = resolvedClientId;
5017
+ }
4994
5018
  const [updated] = await db.update(agents).set(updates).where(eq(agents.uuid, agent.uuid)).returning();
4995
5019
  if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
4996
5020
  return updated;
@@ -5982,7 +6006,7 @@ async function adminAgentRoutes(app) {
5982
6006
  };
5983
6007
  if (health === "disconnected") return reply.status(200).send({
5984
6008
  status: "offline",
5985
- message: "Agent is not connected. Start the client with: first-tree-hub connect <server-url>",
6009
+ message: "Agent is not connected. Start the client with: first-tree-hub client connect <server-url>",
5986
6010
  connection
5987
6011
  });
5988
6012
  if (health === "stale") return reply.status(200).send({
@@ -8226,8 +8250,9 @@ function clientWsRoutes(notifier, instanceId) {
8226
8250
  inboxId: agents.inboxId,
8227
8251
  status: agents.status,
8228
8252
  clientId: agents.clientId,
8229
- clientUserId: clients.userId
8230
- }).from(agents).leftJoin(clients, eq(agents.clientId, clients.id)).where(and(eq(agents.uuid, bindRequest.agentId))).limit(1);
8253
+ clientUserId: clients.userId,
8254
+ managerUserId: members.userId
8255
+ }).from(agents).leftJoin(clients, eq(agents.clientId, clients.id)).leftJoin(members, eq(agents.managerId, members.id)).where(and(eq(agents.uuid, bindRequest.agentId))).limit(1);
8231
8256
  if (!agent) {
8232
8257
  sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.UNKNOWN_AGENT);
8233
8258
  return;
@@ -8240,11 +8265,22 @@ function clientWsRoutes(notifier, instanceId) {
8240
8265
  sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.AGENT_SUSPENDED);
8241
8266
  return;
8242
8267
  }
8243
- if (!agent.clientId || agent.clientId !== clientId) {
8268
+ if (agent.clientId === null) {
8269
+ if (!agent.managerUserId || agent.managerUserId !== session.userId) {
8270
+ sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.NOT_OWNED);
8271
+ return;
8272
+ }
8273
+ if ((await app.db.update(agents).set({
8274
+ clientId,
8275
+ updatedAt: /* @__PURE__ */ new Date()
8276
+ }).where(and(eq(agents.uuid, agent.id), isNull(agents.clientId))).returning({ uuid: agents.uuid })).length === 0) {
8277
+ sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.WRONG_CLIENT);
8278
+ return;
8279
+ }
8280
+ } else if (agent.clientId !== clientId) {
8244
8281
  sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.WRONG_CLIENT);
8245
8282
  return;
8246
- }
8247
- if (!agent.clientUserId || agent.clientUserId !== session.userId) {
8283
+ } else if (!agent.clientUserId || agent.clientUserId !== session.userId) {
8248
8284
  sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.NOT_OWNED);
8249
8285
  return;
8250
8286
  }
@@ -8601,7 +8637,7 @@ async function meRoutes(app) {
8601
8637
  return {
8602
8638
  token,
8603
8639
  expiresIn,
8604
- command: `first-tree-hub connect ${`${request.headers["x-forwarded-proto"] ?? request.protocol}://${request.headers["x-forwarded-host"] ?? request.headers.host ?? request.hostname}`} --token ${token}`
8640
+ command: `first-tree-hub client connect ${`${request.headers["x-forwarded-proto"] ?? request.protocol}://${request.headers["x-forwarded-host"] ?? request.headers.host ?? request.hostname}`} --token ${token}`
8605
8641
  };
8606
8642
  });
8607
8643
  }