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

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.
@@ -550,7 +550,7 @@ function isTokenExpired(token) {
550
550
  if (parts.length !== 3 || !parts[1]) return true;
551
551
  const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
552
552
  if (!payload.exp) return false;
553
- return payload.exp * 1e3 < Date.now() - 3e4;
553
+ return payload.exp * 1e3 < Date.now() + 3e4;
554
554
  } catch {
555
555
  return true;
556
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-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";
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 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-C05B8FzH.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";
@@ -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".`);
@@ -774,7 +798,7 @@ async function authenticateInteractive(url) {
774
798
  return await loginRes.json();
775
799
  }
776
800
  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) => {
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) => {
778
802
  try {
779
803
  const url = serverUrl.replace(/\/+$/, "");
780
804
  setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", url);
@@ -790,12 +814,23 @@ function registerConnectCommand(parent) {
790
814
  schema: clientConfigSchema,
791
815
  role: "client"
792
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`);
793
829
  const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
794
830
  const agents = loadAgents({
795
831
  schema: agentConfigSchema,
796
832
  agentsDir
797
833
  });
798
- process.stderr.write(`\n Starting client (id: ${config.client.id})...\n`);
799
834
  const runtime = new ClientRuntime(config.server.url, config.client.id);
800
835
  for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
801
836
  await runtime.start();
@@ -899,21 +934,63 @@ function registerClientCommands(program) {
899
934
  process.stderr.write(" No agents directory found.\n");
900
935
  }
901
936
  });
902
- client.command("hub-list").description("List connected clients on the Hub server").option("--server <url>", "Hub server URL").action(async (options) => {
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
+ });
979
+ client.command("hub-list").description("List clients on the Hub server").option("--server <url>", "Hub server URL").action(async (options) => {
903
980
  try {
904
981
  const serverUrl = resolveServerUrl(options.server);
905
982
  const token = await ensureFreshAccessToken();
906
- const response = await fetch(`${serverUrl}/api/v1/admin/clients`, {
983
+ const response = await fetch(`${serverUrl}/api/v1/clients`, {
907
984
  headers: { Authorization: `Bearer ${token}` },
908
985
  signal: AbortSignal.timeout(1e4)
909
986
  });
910
987
  if (!response.ok) fail("FETCH_ERROR", `Server returned ${response.status}`, 1);
911
988
  const clients = await response.json();
912
989
  if (clients.length === 0) {
913
- process.stderr.write(" No connected clients.\n");
990
+ process.stderr.write(" No clients.\n");
914
991
  return;
915
992
  }
916
- process.stderr.write(`\n Connected Clients: ${clients.length}\n\n`);
993
+ process.stderr.write(`\n Clients: ${clients.length}\n\n`);
917
994
  const header = ` ${"CLIENT".padEnd(20)} ${"HOST".padEnd(25)} ${"AGENTS".padEnd(8)} CONNECTED`;
918
995
  process.stderr.write(`${header}\n`);
919
996
  process.stderr.write(` ${"─".repeat(header.length - 2)}\n`);
@@ -930,7 +1007,7 @@ function registerClientCommands(program) {
930
1007
  try {
931
1008
  const serverUrl = resolveServerUrl(options.server);
932
1009
  const token = await ensureFreshAccessToken();
933
- const response = await fetch(`${serverUrl}/api/v1/admin/clients/${clientId}/disconnect`, {
1010
+ const response = await fetch(`${serverUrl}/api/v1/clients/${clientId}/disconnect`, {
934
1011
  method: "POST",
935
1012
  headers: { Authorization: `Bearer ${token}` },
936
1013
  signal: AbortSignal.timeout(1e4)
@@ -1049,13 +1126,13 @@ function isSecretField(schema, dotPath) {
1049
1126
  //#region src/commands/onboard.ts
1050
1127
  async function promptMissing(args) {
1051
1128
  if (!args.server) try {
1052
- const { resolveServerUrl } = await import("../bootstrap-DNL1cEwv.mjs").then((n) => n.t);
1129
+ const { resolveServerUrl } = await import("../bootstrap-CRDR6NwE.mjs").then((n) => n.t);
1053
1130
  resolveServerUrl();
1054
1131
  } catch {
1055
1132
  args.server = await input({ message: "Hub server URL:" });
1056
1133
  saveOnboardState(args);
1057
1134
  }
1058
- const { loadCredentials } = await import("../bootstrap-DNL1cEwv.mjs").then((n) => n.t);
1135
+ const { loadCredentials } = await import("../bootstrap-CRDR6NwE.mjs").then((n) => n.t);
1059
1136
  if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub client connect <server-url>` before onboarding.");
1060
1137
  if (!args.id) {
1061
1138
  args.id = await input({ message: "Agent ID:" });