@agent-team-foundation/first-tree-hub 0.8.5 → 0.9.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.
@@ -1,6 +1,6 @@
1
1
  import { d as __exportAll } from "./esm-CYu4tXXn.mjs";
2
2
  import { a as logLevelSchema, i as logFormatSchema } from "./logger-core-2yeIU1fc-B-__AsQO.mjs";
3
- import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
+ import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
5
  import { z } from "zod";
6
6
  import { parse, stringify } from "yaml";
@@ -28,7 +28,7 @@ function defineConfig(shape) {
28
28
  return shape;
29
29
  }
30
30
  /**
31
- * Agent config layout on disk: `~/.first-tree-hub/config/agents/<name>/agent.yaml`.
31
+ * Agent config layout on disk: `~/.first-tree/hub/config/agents/<name>/agent.yaml`.
32
32
  *
33
33
  * After the unified-user-token milestone the local config no longer stores an
34
34
  * agent bearer; authentication comes from the user's member JWT in
@@ -45,10 +45,28 @@ const agentConfigSchema = defineConfig({
45
45
  max_sessions: field(z.number().int().positive().default(10))
46
46
  }
47
47
  });
48
+ /**
49
+ * Phase-dependent defaults that flip with release milestones. Kept as a plain
50
+ * module-level constant so reviews of the beta→GA transition are a one-line
51
+ * diff, and so tests can mock this module to exercise both branches.
52
+ */
53
+ const UPDATE_POLICIES = [
54
+ "auto",
55
+ "prompt",
56
+ "off"
57
+ ];
58
+ /**
59
+ * Default value of `update.policy` on the Client config. During the beta this
60
+ * is `"auto"` — operators rarely know to `npm i -g` weekly and we chase the
61
+ * latest published Command by default. The GA PR flips it to `"prompt"` and
62
+ * bumps Command to `1.0.0`.
63
+ */
64
+ const UPDATE_POLICY_DEFAULT = "auto";
48
65
  /** Store the resolved config as a singleton. Called by initConfig(). */
49
66
  function setConfig(config) {}
50
67
  /** Reset the config singleton. For testing only. */
51
68
  function resetConfig() {}
69
+ const updatePolicySchema = z.enum(UPDATE_POLICIES);
52
70
  const clientConfigSchema = defineConfig({
53
71
  server: { url: field(z.string(), {
54
72
  env: "FIRST_TREE_HUB_SERVER_URL",
@@ -61,9 +79,15 @@ const clientConfigSchema = defineConfig({
61
79
  auto: "client-id",
62
80
  env: "FIRST_TREE_HUB_CLIENT_ID"
63
81
  }) },
82
+ update: {
83
+ policy: field(updatePolicySchema.default(UPDATE_POLICY_DEFAULT), { env: "FIRST_TREE_HUB_UPDATE_POLICY" }),
84
+ restart_quiet_seconds: field(z.number().int().min(1).max(3600).default(30), { env: "FIRST_TREE_HUB_UPDATE_RESTART_QUIET_SECONDS" }),
85
+ restart_check_interval_seconds: field(z.number().int().min(5).max(300).default(10), { env: "FIRST_TREE_HUB_UPDATE_RESTART_CHECK_INTERVAL_SECONDS" }),
86
+ prompt_timeout_seconds: field(z.number().int().min(10).max(600).default(60), { env: "FIRST_TREE_HUB_UPDATE_PROMPT_TIMEOUT_SECONDS" })
87
+ },
64
88
  logLevel: field(logLevelSchema.default("info"), { env: "FIRST_TREE_HUB_LOG_LEVEL" })
65
89
  });
66
- const DEFAULT_HOME_DIR = process.env.FIRST_TREE_HUB_HOME ?? join(homedir(), ".first-tree-hub");
90
+ const DEFAULT_HOME_DIR = process.env.FIRST_TREE_HUB_HOME ?? join(homedir(), ".first-tree", "hub");
67
91
  const DEFAULT_CONFIG_DIR = join(DEFAULT_HOME_DIR, "config");
68
92
  const DEFAULT_DATA_DIR = join(DEFAULT_HOME_DIR, "data");
69
93
  function isFieldDef(value) {
@@ -407,6 +431,102 @@ function loadAgents(options) {
407
431
  }
408
432
  return result;
409
433
  }
434
+ /**
435
+ * Pre-v0.9 path. The home directory was a flat `~/.first-tree-hub/` — this
436
+ * was renamed to `~/.first-tree/hub/` so the `.first-tree/` parent can be
437
+ * shared with sibling products (context-tree etc.) under the same brand.
438
+ */
439
+ const LEGACY_HOME_DIR = join(homedir(), ".first-tree-hub");
440
+ /**
441
+ * Auto-migrate the legacy `~/.first-tree-hub/` home to the new
442
+ * `~/.first-tree/hub/` layout. Designed to be called once at CLI startup —
443
+ * idempotent, never throws, and skips any case that could merge state.
444
+ *
445
+ * **Copy-only semantics:** the legacy tree is preserved on disk as a safety
446
+ * net. Users can inspect or fall back to it, and can delete it manually
447
+ * once they've confirmed the new location is healthy. Idempotency is
448
+ * therefore keyed on whether the *target* already has content, not on
449
+ * whether the legacy path still exists.
450
+ *
451
+ * Skip rules (in order):
452
+ * 1. `FIRST_TREE_HUB_HOME` is set → user is driving the path explicitly.
453
+ * 2. Legacy path doesn't exist → nothing to migrate.
454
+ * 3. New path already has content → treat as already-migrated (or a
455
+ * conflict the user must resolve). Either way, never merge.
456
+ *
457
+ * Otherwise we recursively copy legacy → new with `cpSync`, preserving
458
+ * mtimes so log rotation and mtime heuristics keep working.
459
+ */
460
+ /**
461
+ * Walk `src` depth-first and mirror every directory's mode onto the matching
462
+ * path in `dest`. Skips symlinks (we don't want to chmod whatever they
463
+ * resolve to — that can reach outside the tree). Files are left alone
464
+ * because `cpSync` already preserves file modes.
465
+ */
466
+ function syncDirectoryModes(src, dest) {
467
+ chmodSync(dest, statSync(src).mode & 4095);
468
+ const stack = [""];
469
+ while (stack.length > 0) {
470
+ const rel = stack.pop();
471
+ if (rel === void 0) break;
472
+ const srcDir = join(src, rel);
473
+ for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
474
+ if (!entry.isDirectory()) continue;
475
+ const relChild = rel === "" ? entry.name : join(rel, entry.name);
476
+ const mode = statSync(join(src, relChild)).mode & 4095;
477
+ chmodSync(join(dest, relChild), mode);
478
+ stack.push(relChild);
479
+ }
480
+ }
481
+ }
482
+ function migrateLegacyHome(opts) {
483
+ const { newHome, envOverride } = opts;
484
+ const legacyHome = opts.legacyHome ?? LEGACY_HOME_DIR;
485
+ if (envOverride) return {
486
+ migrated: false,
487
+ reason: "custom-home"
488
+ };
489
+ if (!existsSync(legacyHome)) return {
490
+ migrated: false,
491
+ reason: "no-legacy-dir"
492
+ };
493
+ if (existsSync(newHome)) try {
494
+ if (readdirSync(newHome).length > 0) return {
495
+ migrated: false,
496
+ reason: "new-dir-populated",
497
+ from: legacyHome,
498
+ to: newHome
499
+ };
500
+ } catch (err) {
501
+ return {
502
+ migrated: false,
503
+ reason: "failed",
504
+ from: legacyHome,
505
+ to: newHome,
506
+ error: err instanceof Error ? err.message : String(err)
507
+ };
508
+ }
509
+ try {
510
+ cpSync(legacyHome, newHome, {
511
+ recursive: true,
512
+ preserveTimestamps: true
513
+ });
514
+ syncDirectoryModes(legacyHome, newHome);
515
+ return {
516
+ migrated: true,
517
+ from: legacyHome,
518
+ to: newHome
519
+ };
520
+ } catch (err) {
521
+ return {
522
+ migrated: false,
523
+ reason: "failed",
524
+ from: legacyHome,
525
+ to: newHome,
526
+ error: err instanceof Error ? err.message : String(err)
527
+ };
528
+ }
529
+ }
410
530
  const serverConfigSchema = defineConfig({
411
531
  database: {
412
532
  url: field(z.string(), {
@@ -622,4 +742,4 @@ function saveAgentConfig(agentName, agentId, runtime) {
622
742
  return agentDir;
623
743
  }
624
744
  //#endregion
625
- export { setConfigValue as C, serverConfigSchema as S, loadAgents as _, resolveAccessToken as a, resetConfigMeta as b, saveCredentials as c, DEFAULT_HOME_DIR as d, agentConfigSchema as f, initConfig as g, getConfigValue as h, loadCredentials as i, DEFAULT_CONFIG_DIR as l, collectMissingPrompts as m, ensureFreshAccessToken as n, resolveServerUrl as o, clientConfigSchema as p, ensureFreshAdminToken as r, saveAgentConfig as s, bootstrap_exports as t, DEFAULT_DATA_DIR as u, readConfigFile as v, resolveConfigReadonly as x, resetConfig as y };
745
+ export { serverConfigSchema as C, resolveConfigReadonly as S, loadAgents as _, resolveAccessToken as a, resetConfig as b, saveCredentials as c, DEFAULT_HOME_DIR as d, agentConfigSchema as f, initConfig as g, getConfigValue as h, loadCredentials as i, DEFAULT_CONFIG_DIR as l, collectMissingPrompts as m, ensureFreshAccessToken as n, resolveServerUrl as o, clientConfigSchema as p, ensureFreshAdminToken as r, saveAgentConfig as s, bootstrap_exports as t, DEFAULT_DATA_DIR as u, migrateLegacyHome as v, setConfigValue as w, resetConfigMeta as x, readConfigFile as y };
@@ -1,10 +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, 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";
3
+ import { C as serverConfigSchema, _ as loadAgents, b as resetConfig, 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, w as setConfigValue, x as resetConfigMeta, y as readConfigFile } from "../bootstrap-CWcBzk6C.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-DoIprl2f.mjs";
6
- import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-n9Y2yGTT.mjs";
7
- import { createRequire } from "node:module";
5
+ import { A as printResults, B as SdkError, C as checkDatabase, D as checkServerHealth, E as checkServerConfig, F as stopPostgres, H as cleanWorkspaces, I as ClientRuntime, L as createOwner, O as checkServerReachable, S as checkClientConfig, T as checkNodeVersion, U as applyClientLoggerConfig, V as SessionRegistry, _ as isServiceSupported, a as COMMAND_VERSION, b as runMigrations, c as promptMissingFields, d as onboardCheck, f as onboardCreate, g as installClientService, h as getClientServiceStatus, i as startServer, k as checkWebSocket, l as formatCheckReport, m as runHomeMigration, n as declineUpdate, o as isInteractive, p as saveOnboardState, r as promptUpdate, s as promptAddAgent, t as createExecuteUpdate, u as loadOnboardState, w as checkDocker, x as checkAgentConfigs, y as uninstallClientService, z as FirstTreeHubSDK } from "../core-DZDhomaN.mjs";
6
+ import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-GlaczcVf.mjs";
8
7
  import { Command } from "commander";
9
8
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
10
9
  import { join } from "node:path";
@@ -781,7 +780,7 @@ function decodeJwtPayload(token) {
781
780
  * and give the operator a chance to back out before we overwrite credentials.
782
781
  *
783
782
  * Why this gate exists: running `client connect` implicitly overwrites
784
- * `~/.first-tree-hub/config/credentials.json`. Without this prompt, someone
783
+ * `~/.first-tree/hub/config/credentials.json`. Without this prompt, someone
785
784
  * onboarding a second account on their own machine silently logs themselves
786
785
  * out of the first account — they'd only notice when their "main" agents
787
786
  * appeared offline later. We treat single-account-per-machine as the product
@@ -839,7 +838,7 @@ async function promptReplaceOrCancel(newMemberId, newServerUrl) {
839
838
  function printIsolationGuide(newServerUrl) {
840
839
  process.stderr.write("\n Cancelled. The existing account on this computer is untouched.\n\n");
841
840
  process.stderr.write(" To run this new account alongside it (advanced — no background service):\n\n");
842
- process.stderr.write(" export FIRST_TREE_HUB_HOME=\"$HOME/.first-tree-hub-<label>\"\n");
841
+ process.stderr.write(" export FIRST_TREE_HUB_HOME=\"$HOME/.first-tree/hub-<label>\"\n");
843
842
  process.stderr.write(` first-tree-hub client connect ${newServerUrl} --token <token>\n`);
844
843
  process.stderr.write(" first-tree-hub client start\n\n");
845
844
  process.stderr.write(" Notes:\n");
@@ -930,7 +929,14 @@ function registerConnectCommand(parent) {
930
929
  schema: agentConfigSchema,
931
930
  agentsDir
932
931
  });
933
- const runtime = new ClientRuntime(config.server.url, config.client.id);
932
+ const runtime = new ClientRuntime(config.server.url, config.client.id, {
933
+ currentVersion: COMMAND_VERSION,
934
+ update: {
935
+ updateConfig: config.update,
936
+ prompt: promptUpdate,
937
+ executeUpdate: createExecuteUpdate({ managed: false })
938
+ }
939
+ });
934
940
  for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
935
941
  await runtime.start();
936
942
  runtime.watchAgentsDir(agentsDir);
@@ -980,7 +986,15 @@ function registerClientCommands(program) {
980
986
  agentsDir
981
987
  });
982
988
  process.stderr.write(`\n Connecting to ${config.server.url} (client id: ${config.client.id})...\n`);
983
- const runtime = new ClientRuntime(config.server.url, config.client.id);
989
+ const managed = options.interactive === false;
990
+ const runtime = new ClientRuntime(config.server.url, config.client.id, {
991
+ currentVersion: COMMAND_VERSION,
992
+ update: {
993
+ updateConfig: config.update,
994
+ prompt: managed ? declineUpdate : promptUpdate,
995
+ executeUpdate: createExecuteUpdate({ managed })
996
+ }
997
+ });
984
998
  for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
985
999
  await runtime.start();
986
1000
  runtime.watchAgentsDir(agentsDir);
@@ -1226,13 +1240,13 @@ function isSecretField(schema, dotPath) {
1226
1240
  //#region src/commands/onboard.ts
1227
1241
  async function promptMissing(args) {
1228
1242
  if (!args.server) try {
1229
- const { resolveServerUrl } = await import("../bootstrap-99vUYmLs.mjs").then((n) => n.t);
1243
+ const { resolveServerUrl } = await import("../bootstrap-CWcBzk6C.mjs").then((n) => n.t);
1230
1244
  resolveServerUrl();
1231
1245
  } catch {
1232
1246
  args.server = await input({ message: "Hub server URL:" });
1233
1247
  saveOnboardState(args);
1234
1248
  }
1235
- const { loadCredentials } = await import("../bootstrap-99vUYmLs.mjs").then((n) => n.t);
1249
+ const { loadCredentials } = await import("../bootstrap-CWcBzk6C.mjs").then((n) => n.t);
1236
1250
  if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub client connect <server-url>` before onboarding.");
1237
1251
  if (!args.id) {
1238
1252
  args.id = await input({ message: "Agent ID:" });
@@ -1492,9 +1506,9 @@ function formatUptime(seconds) {
1492
1506
  }
1493
1507
  //#endregion
1494
1508
  //#region src/cli/index.ts
1495
- const { version } = createRequire(import.meta.url)("../../package.json");
1509
+ runHomeMigration();
1496
1510
  const program = new Command();
1497
- program.name("first-tree-hub").description("First Tree Hub — centralized collaboration platform for agent teams").version(version);
1511
+ program.name("first-tree-hub").description("First Tree Hub — centralized collaboration platform for agent teams").version(COMMAND_VERSION);
1498
1512
  registerServerCommands(program);
1499
1513
  registerClientCommands(program);
1500
1514
  registerAgentCommands(program);