@agent-team-foundation/first-tree-hub 0.7.2 → 0.8.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.
@@ -520,6 +520,14 @@ function resolveAccessToken() {
520
520
  return creds.accessToken;
521
521
  }
522
522
  /**
523
+ * In-flight refresh promise. Multiple callers (WS handshake, proactive
524
+ * refresh timer, every SDK request) can see an expired token within the same
525
+ * millisecond — without dedupe each would fire an independent `/auth/refresh`
526
+ * round-trip and race to write `credentials.json`. Share one in-flight
527
+ * promise so N concurrent callers resolve from a single HTTP call.
528
+ */
529
+ let inflightRefresh = null;
530
+ /**
523
531
  * Ensure the persisted access token is fresh. Call before any API request
524
532
  * when using persisted credentials. Returns the (possibly refreshed) access
525
533
  * token. Service-user API keys are out of scope for this milestone.
@@ -528,19 +536,27 @@ async function ensureFreshAccessToken() {
528
536
  const creds = loadCredentials();
529
537
  if (!creds) throw new Error("No credentials found. Run `first-tree-hub client connect <server-url>` to sign in.");
530
538
  if (!isTokenExpired(creds.accessToken)) return creds.accessToken;
531
- const res = await fetch(`${creds.serverUrl}/api/v1/auth/refresh`, {
532
- method: "POST",
533
- headers: { "Content-Type": "application/json" },
534
- body: JSON.stringify({ refreshToken: creds.refreshToken }),
535
- signal: AbortSignal.timeout(1e4)
536
- });
537
- if (!res.ok) throw new Error("Access token expired and refresh failed. Run `first-tree-hub client connect <server-url>`.");
538
- const data = await res.json();
539
- saveCredentials({
540
- ...creds,
541
- accessToken: data.accessToken
542
- });
543
- return data.accessToken;
539
+ if (inflightRefresh) return inflightRefresh;
540
+ inflightRefresh = (async () => {
541
+ const res = await fetch(`${creds.serverUrl}/api/v1/auth/refresh`, {
542
+ method: "POST",
543
+ headers: { "Content-Type": "application/json" },
544
+ body: JSON.stringify({ refreshToken: creds.refreshToken }),
545
+ signal: AbortSignal.timeout(1e4)
546
+ });
547
+ if (!res.ok) throw new Error("Access token expired and refresh failed. Run `first-tree-hub client connect <server-url>`.");
548
+ const data = await res.json();
549
+ saveCredentials({
550
+ ...creds,
551
+ accessToken: data.accessToken
552
+ });
553
+ return data.accessToken;
554
+ })();
555
+ try {
556
+ return await inflightRefresh;
557
+ } finally {
558
+ inflightRefresh = null;
559
+ }
544
560
  }
545
561
  /** Back-compat alias retained so existing call sites keep compiling. */
546
562
  const ensureFreshAdminToken = ensureFreshAccessToken;
@@ -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-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-6-paFwyo.mjs";
4
- import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-CJ08ntOD.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-8nCntTrK.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-BA5U1v9L.mjs";
4
+ import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-D9JkMZnU.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";
@@ -1126,13 +1126,13 @@ function isSecretField(schema, dotPath) {
1126
1126
  //#region src/commands/onboard.ts
1127
1127
  async function promptMissing(args) {
1128
1128
  if (!args.server) try {
1129
- const { resolveServerUrl } = await import("../bootstrap-CRDR6NwE.mjs").then((n) => n.t);
1129
+ const { resolveServerUrl } = await import("../bootstrap-8nCntTrK.mjs").then((n) => n.t);
1130
1130
  resolveServerUrl();
1131
1131
  } catch {
1132
1132
  args.server = await input({ message: "Hub server URL:" });
1133
1133
  saveOnboardState(args);
1134
1134
  }
1135
- const { loadCredentials } = await import("../bootstrap-CRDR6NwE.mjs").then((n) => n.t);
1135
+ const { loadCredentials } = await import("../bootstrap-8nCntTrK.mjs").then((n) => n.t);
1136
1136
  if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub client connect <server-url>` before onboarding.");
1137
1137
  if (!args.id) {
1138
1138
  args.id = await input({ message: "Agent ID:" });
@@ -1,9 +1,9 @@
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-CRDR6NwE.mjs";
2
- import { $ as updateAgentSchema, 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 sessionEventSchema$1, K as sessionCompletionMessageSchema, L as loginSchema, M as delegateFeishuUserSchema, N as dryRunAgentRuntimeConfigSchema, O as createChatSchema, P as inboxPollQuerySchema, Q as updateAgentRuntimeConfigSchema, R as messageSourceSchema$1, S as agentTypeSchema$1, T as createAdapterConfigSchema, U as selfServiceFeishuBotSchema, V as refreshTokenSchema, W as sendMessageSchema, X as taskListQuerySchema, Y as sessionStateMessageSchema, Z as updateAdapterConfigSchema, _ as addParticipantSchema, a as AGENT_SELECTOR_HEADER$1, b as agentBindRequestSchema, c as AGENT_TYPES, d as SYSTEM_CONFIG_DEFAULTS, et as updateMemberSchema, f as TASK_CREATOR_TYPES, g as WS_AUTH_FRAME_TIMEOUT_MS, h as TASK_TERMINAL_STATUSES, i as AGENT_BIND_REJECT_REASONS, it as wsAuthFrameSchema, j as createTaskSchema, k as createMemberSchema, l as AGENT_VISIBILITY, m as TASK_STATUSES, nt as updateSystemConfigSchema, o as AGENT_SOURCES, p as TASK_HEALTH_SIGNALS, q as sessionEventMessageSchema, rt as updateTaskStatusSchema, s as AGENT_STATUSES, tt as updateOrganizationSchema, 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-CJ08ntOD.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-8nCntTrK.mjs";
2
+ import { $ as updateAgentRuntimeConfigSchema, A as createMemberSchema, B as notificationQuerySchema, C as agentTypeSchema$1, D as createAdapterMappingSchema, E as createAdapterConfigSchema, F as inboxPollQuerySchema, G as sendMessageSchema, H as refreshTokenSchema, I as isRedactedEnvValue, J as sessionEventMessageSchema, K as sendToAgentSchema, L as linkTaskChatSchema, M as createTaskSchema, N as delegateFeishuUserSchema, O as createAgentSchema, P as dryRunAgentRuntimeConfigSchema, Q as updateAdapterConfigSchema, R as loginSchema, S as agentRuntimeConfigPayloadSchema$1, T as connectTokenExchangeSchema, U as runtimeStateMessageSchema, V as paginationQuerySchema, W as selfServiceFeishuBotSchema, X as sessionStateMessageSchema, Y as sessionEventSchema$1, Z as taskListQuerySchema, _ as addParticipantSchema, a as AGENT_SELECTOR_HEADER$1, at as wsAuthFrameSchema, b as agentBindRequestSchema, c as AGENT_TYPES, d as SYSTEM_CONFIG_DEFAULTS, et as updateAgentSchema, f as TASK_CREATOR_TYPES, g as WS_AUTH_FRAME_TIMEOUT_MS, h as TASK_TERMINAL_STATUSES, i as AGENT_BIND_REJECT_REASONS, it as updateTaskStatusSchema, j as createOrganizationSchema, k as createChatSchema, l as AGENT_VISIBILITY, m as TASK_STATUSES, nt as updateOrganizationSchema, o as AGENT_SOURCES, p as TASK_HEALTH_SIGNALS, q as sessionCompletionMessageSchema, rt as updateSystemConfigSchema, s as AGENT_STATUSES, tt as updateMemberSchema, u as DEFAULT_AGENT_RUNTIME_CONFIG_PAYLOAD, v as adminCreateTaskSchema, w as clientRegisterSchema, x as agentPinnedMessageSchema$1, y as adminUpdateTaskSchema, z as messageSourceSchema$1 } from "./feishu-D9JkMZnU.mjs";
3
3
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
4
4
  import { dirname, isAbsolute, join, resolve } from "node:path";
5
5
  import { ZodError, z } from "zod";
6
- import "yaml";
6
+ import { stringify } from "yaml";
7
7
  import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
8
8
  import { homedir, hostname, platform, userInfo } from "node:os";
9
9
  import { EventEmitter } from "node:events";
@@ -198,6 +198,19 @@ z.object({
198
198
  branch: z.string().nullable()
199
199
  });
200
200
  /**
201
+ * Server → client WebSocket frame announcing that an agent has just been
202
+ * pinned to the connected client (either created with `clientId` or bound via
203
+ * PATCH NULL → ID). The client can auto-register a local config from this so
204
+ * the operator doesn't have to run `first-tree-hub agent add` manually.
205
+ */
206
+ const agentPinnedMessageSchema = z.object({
207
+ type: z.literal("agent:pinned"),
208
+ agentId: z.string(),
209
+ name: z.string().nullable(),
210
+ displayName: z.string().nullable(),
211
+ agentType: agentTypeSchema
212
+ });
213
+ /**
201
214
  * Agent runtime configuration — M1 (Claude Code only).
202
215
  *
203
216
  * Defines the 5 user-tunable field groups that the Hub centrally manages
@@ -933,6 +946,7 @@ var ClientConnection = class extends EventEmitter {
933
946
  this.serverUrl = config.serverUrl.replace(/\/+$/, "");
934
947
  this.sdkVersion = config.sdkVersion;
935
948
  this.getAccessToken = config.getAccessToken;
949
+ this.on("error", () => {});
936
950
  }
937
951
  get isConnected() {
938
952
  return this.ws !== null && this.ws.readyState === WebSocket.OPEN && this.registered;
@@ -1165,6 +1179,11 @@ var ClientConnection = class extends EventEmitter {
1165
1179
  this.emit("agent:unbound", agentId);
1166
1180
  return;
1167
1181
  }
1182
+ if (type === "agent:pinned") {
1183
+ const parsed = agentPinnedMessageSchema.safeParse(msg);
1184
+ if (parsed.success) this.emit("agent:pinned", parsed.data);
1185
+ return;
1186
+ }
1168
1187
  if (type === "agent:force_disconnect") {
1169
1188
  const agentId = msg.agentId;
1170
1189
  if (agentId && this.boundAgents.has(agentId)) {
@@ -1440,6 +1459,75 @@ function bootstrapWorkspace(options) {
1440
1459
  }
1441
1460
  writeFileSync(join(agentDir, "tools.md"), generateToolsDoc(), "utf-8");
1442
1461
  }
1462
+ function defaultInstallExec(command, args, options) {
1463
+ execFileSync(command, args, {
1464
+ cwd: options.cwd,
1465
+ stdio: "pipe",
1466
+ timeout: options.timeout,
1467
+ encoding: "utf-8"
1468
+ });
1469
+ }
1470
+ /**
1471
+ * Install the first-tree skill and FIRST-TREE-SOURCE-INTEGRATION block into
1472
+ * the workspace by shelling out to `first-tree tree integrate`.
1473
+ *
1474
+ * Resolution order for the CLI binary:
1475
+ * 1. `first-tree` on PATH — preferred for runtime images that pre-install it.
1476
+ * 2. `npx -y first-tree@latest` — fallback that downloads on first run.
1477
+ *
1478
+ * Graceful degradation: returns false on failure and logs. The session still
1479
+ * starts; the agent just doesn't have the first-tree skill wired up.
1480
+ */
1481
+ function installFirstTreeIntegration(options) {
1482
+ const { workspacePath, contextTreePath, workspaceId, treeRepoUrl, log } = options;
1483
+ const exec = options.exec ?? defaultInstallExec;
1484
+ const integrateArgs = [
1485
+ "tree",
1486
+ "integrate",
1487
+ "--source-path",
1488
+ workspacePath,
1489
+ "--tree-path",
1490
+ contextTreePath,
1491
+ "--mode",
1492
+ "workspace-root",
1493
+ "--workspace-id",
1494
+ workspaceId,
1495
+ ...treeRepoUrl ? ["--tree-url", treeRepoUrl] : []
1496
+ ];
1497
+ const attempts = [{
1498
+ command: "first-tree",
1499
+ args: integrateArgs,
1500
+ label: "first-tree (PATH)"
1501
+ }, {
1502
+ command: "npx",
1503
+ args: [
1504
+ "-y",
1505
+ "first-tree@latest",
1506
+ ...integrateArgs
1507
+ ],
1508
+ label: "npx first-tree@latest"
1509
+ }];
1510
+ for (let index = 0; index < attempts.length; index += 1) {
1511
+ const attempt = attempts[index];
1512
+ if (!attempt) continue;
1513
+ try {
1514
+ exec(attempt.command, attempt.args, {
1515
+ cwd: workspacePath,
1516
+ timeout: 12e4
1517
+ });
1518
+ log(`First-tree integration installed via ${attempt.label}`);
1519
+ return true;
1520
+ } catch (err) {
1521
+ const msg = err instanceof Error ? err.message : String(err);
1522
+ const binaryMissing = /ENOENT|not found|command not found/i.test(msg);
1523
+ const isLastAttempt = index === attempts.length - 1;
1524
+ if (binaryMissing && !isLastAttempt) continue;
1525
+ log(`First-tree integration skipped (${attempt.label}): ${msg.slice(0, 200)}`);
1526
+ return false;
1527
+ }
1528
+ }
1529
+ return false;
1530
+ }
1443
1531
  function generateToolsDoc() {
1444
1532
  return `# Agent Hub SDK
1445
1533
 
@@ -2454,6 +2542,12 @@ const createClaudeCodeHandler = (config) => {
2454
2542
  chatId: sessionCtx.chatId
2455
2543
  });
2456
2544
  generateClaudeMd(workspace, sessionCtx.agent, contextTreePath);
2545
+ if (contextTreePath) installFirstTreeIntegration({
2546
+ workspacePath: workspace,
2547
+ contextTreePath,
2548
+ workspaceId: sessionCtx.chatId,
2549
+ log: (msg) => sessionCtx.log(msg)
2550
+ });
2457
2551
  }
2458
2552
  const handler = {
2459
2553
  async start(message, sessionCtx) {
@@ -3445,8 +3539,15 @@ var ClientRuntime = class {
3445
3539
  connection;
3446
3540
  agents = [];
3447
3541
  agentNames = /* @__PURE__ */ new Set();
3542
+ agentIds = /* @__PURE__ */ new Set();
3448
3543
  watcher = null;
3449
3544
  debounceTimer = null;
3545
+ /**
3546
+ * Directory we write auto-registered agent configs into (same path that
3547
+ * `first-tree-hub agent add` uses). Set by `watchAgentsDir` so the
3548
+ * `agent:pinned` handler knows where to materialise new configs.
3549
+ */
3550
+ agentsDir = null;
3450
3551
  constructor(serverUrl, clientId) {
3451
3552
  this.serverUrl = serverUrl;
3452
3553
  this.connection = new ClientConnection({
@@ -3458,6 +3559,12 @@ var ClientRuntime = class {
3458
3559
  this.connection.on("auth:expired", () => {
3459
3560
  process.stderr.write(" ⚠️ Access token expired — reconnecting after refresh...\n");
3460
3561
  });
3562
+ this.connection.on("error", (err) => {
3563
+ process.stderr.write(` \u26A0\uFE0F Client connection error: ${err.message}\n`);
3564
+ });
3565
+ this.connection.on("agent:pinned", (message) => {
3566
+ this.handleAgentPinned(message);
3567
+ });
3461
3568
  }
3462
3569
  addAgent(name, config) {
3463
3570
  if (this.agentNames.has(name)) return;
@@ -3480,6 +3587,7 @@ var ClientRuntime = class {
3480
3587
  slot
3481
3588
  });
3482
3589
  this.agentNames.add(name);
3590
+ this.agentIds.add(config.agentId);
3483
3591
  }
3484
3592
  async start() {
3485
3593
  await this.connection.connect();
@@ -3502,6 +3610,7 @@ var ClientRuntime = class {
3502
3610
  process.stderr.write(`\n ${connected} agent(s) running. Press Ctrl+C to stop.\n`);
3503
3611
  }
3504
3612
  watchAgentsDir(agentsDir) {
3613
+ this.agentsDir = agentsDir;
3505
3614
  if (this.watcher) return;
3506
3615
  if (!existsSync(agentsDir)) return;
3507
3616
  this.watcher = watch(agentsDir, { recursive: true }, () => {
@@ -3535,12 +3644,68 @@ var ClientRuntime = class {
3535
3644
  });
3536
3645
  for (const [name, config] of all) {
3537
3646
  if (this.agentNames.has(name)) continue;
3647
+ if (this.agentIds.has(config.agentId)) continue;
3538
3648
  process.stderr.write(`\n New agent detected: ${name}\n`);
3539
3649
  this.addAgent(name, config);
3540
3650
  this.startAgent(name);
3541
3651
  }
3542
3652
  } catch {}
3543
3653
  }
3654
+ /**
3655
+ * React to an `agent:pinned` server push by writing the local config file
3656
+ * (same shape `first-tree-hub agent add` produces) and scheduling the new
3657
+ * slot — so the operator doesn't have to run `agent add` manually after
3658
+ * creating an agent from the admin UI or API.
3659
+ */
3660
+ handleAgentPinned(message) {
3661
+ if (this.agentIds.has(message.agentId)) return;
3662
+ if (!this.agentsDir) {
3663
+ process.stderr.write(` \u26A0\uFE0F Agent pinned (${message.agentId}) but no agents dir set — cannot auto-register.\n`);
3664
+ return;
3665
+ }
3666
+ const localName = this.pickLocalName(message);
3667
+ const agentDir = join(this.agentsDir, localName);
3668
+ try {
3669
+ mkdirSync(agentDir, {
3670
+ recursive: true,
3671
+ mode: 448
3672
+ });
3673
+ const yaml = stringify({
3674
+ agentId: message.agentId,
3675
+ runtime: "claude-code"
3676
+ });
3677
+ writeFileSync(join(agentDir, "agent.yaml"), yaml, { mode: 384 });
3678
+ process.stderr.write(` \u2713 Auto-added agent "${localName}" (${message.agentId}) from server push.\n`);
3679
+ } catch (err) {
3680
+ const msg = err instanceof Error ? err.message : String(err);
3681
+ process.stderr.write(` \u2717 Failed to auto-add agent "${localName}": ${msg}\n`);
3682
+ return;
3683
+ }
3684
+ this.scanForNewAgents(this.agentsDir);
3685
+ }
3686
+ /**
3687
+ * Choose the directory name under `agents/<name>/agent.yaml` for an agent
3688
+ * pushed by the server. Prefer the server-side `name` when set and not
3689
+ * already claimed; otherwise fall back to a UUID-derived name with a numeric
3690
+ * suffix on collision.
3691
+ *
3692
+ * UUID v7 packs the unix-ms timestamp in the high bits, so two agents
3693
+ * created in the same millisecond share the first 8 hex chars. Take 16 chars
3694
+ * (the full ms-timestamp segment plus the random tail) to make accidental
3695
+ * collisions astronomically unlikely, and re-check `agentNames` so even an
3696
+ * adversarial collision falls through to a `-2`, `-3`, … suffix.
3697
+ */
3698
+ pickLocalName(message) {
3699
+ const preferred = message.name;
3700
+ if (preferred && !this.agentNames.has(preferred)) return preferred;
3701
+ const base = `agent-${message.agentId.replace(/[^a-z0-9]/gi, "").slice(0, 16).toLowerCase()}`;
3702
+ if (!this.agentNames.has(base)) return base;
3703
+ for (let suffix = 2; suffix < 1e3; suffix++) {
3704
+ const candidate = `${base}-${suffix}`;
3705
+ if (!this.agentNames.has(candidate)) return candidate;
3706
+ }
3707
+ return `agent-${message.agentId.replace(/[^a-z0-9]/gi, "").toLowerCase()}`;
3708
+ }
3544
3709
  startAgent(name) {
3545
3710
  const entry = this.agents.find((a) => a.name === name);
3546
3711
  if (!entry) return;
@@ -4206,7 +4371,7 @@ async function onboardCreate(args) {
4206
4371
  }
4207
4372
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
4208
4373
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
4209
- const { bindFeishuBot } = await import("./feishu-CJ08ntOD.mjs").then((n) => n.r);
4374
+ const { bindFeishuBot } = await import("./feishu-D9JkMZnU.mjs").then((n) => n.r);
4210
4375
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
4211
4376
  if (!targetAgentUuid) process.stderr.write(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
4212
4377
  else {
@@ -4347,7 +4512,7 @@ function setNestedByDot(obj, dotPath, value) {
4347
4512
  if (lastKey !== void 0) current[lastKey] = value;
4348
4513
  }
4349
4514
  //#endregion
4350
- //#region ../server/dist/app-CWKBBGod.mjs
4515
+ //#region ../server/dist/app-TMhTLXuz.mjs
4351
4516
  var __defProp = Object.defineProperty;
4352
4517
  var __exportAll = (all, no_symbols) => {
4353
4518
  let target = {};
@@ -5947,6 +6112,23 @@ async function getClient(db, clientId) {
5947
6112
  const [row] = await db.select().from(clients).where(eq(clients.id, clientId)).limit(1);
5948
6113
  return row ?? null;
5949
6114
  }
6115
+ /**
6116
+ * List the active agents currently pinned to a client. Used by the WS
6117
+ * registration handshake to backfill `agent:pinned` notifications missed while
6118
+ * the client was offline — without it, an admin who pinned an agent during a
6119
+ * client outage would still need a manual `first-tree-hub agent add`.
6120
+ *
6121
+ * Excludes soft-deleted agents (status = "deleted"). Human agents are
6122
+ * naturally excluded by the `clientId` filter — they never carry a clientId.
6123
+ */
6124
+ async function listActiveAgentsPinnedToClient(db, clientId) {
6125
+ return db.select({
6126
+ uuid: agents.uuid,
6127
+ name: agents.name,
6128
+ displayName: agents.displayName,
6129
+ type: agents.type
6130
+ }).from(agents).where(and(eq(agents.clientId, clientId), ne(agents.status, "deleted")));
6131
+ }
5950
6132
  async function listClients(db, userId) {
5951
6133
  const rows = await db.select().from(clients).where(eq(clients.userId, userId));
5952
6134
  const counts = await db.select({
@@ -6084,6 +6266,13 @@ function removeClientConnection(clientId, ws) {
6084
6266
  clientConnections.delete(clientId);
6085
6267
  return agentIds;
6086
6268
  }
6269
+ /** Send a message to a client's WebSocket. Returns true if delivered. */
6270
+ function sendToClient(clientId, message) {
6271
+ const entry = clientConnections.get(clientId);
6272
+ if (!entry || entry.ws.readyState !== 1) return false;
6273
+ entry.ws.send(JSON.stringify(message));
6274
+ return true;
6275
+ }
6087
6276
  /** Send a message to a specific agent via its client's WebSocket. Returns true if delivered. */
6088
6277
  function sendToAgent$1(agentId, message) {
6089
6278
  const clientId = agentToClient.get(agentId);
@@ -6350,6 +6539,34 @@ function notifyRecipients(notifier, recipients, messageId) {
6350
6539
  for (const inboxId of recipients) notifier.notify(inboxId, messageId).catch(() => {});
6351
6540
  }
6352
6541
  async function adminAgentRoutes(app) {
6542
+ /**
6543
+ * Push an `agent:pinned` frame to the connected client so it can auto-register
6544
+ * the agent locally without the operator running `first-tree-hub agent add`.
6545
+ *
6546
+ * Best-effort: if the client is not currently connected to this server
6547
+ * instance, the notification is silently dropped here — the client picks the
6548
+ * pinning up on its next `client:register` handshake via the backfill path
6549
+ * in `api/agent/ws-client.ts`.
6550
+ */
6551
+ function notifyClientAgentPinned(agent) {
6552
+ if (!agent.clientId) return;
6553
+ const parsed = agentPinnedMessageSchema$1.safeParse({
6554
+ type: "agent:pinned",
6555
+ agentId: agent.uuid,
6556
+ name: agent.name,
6557
+ displayName: agent.displayName,
6558
+ agentType: agent.type
6559
+ });
6560
+ if (!parsed.success) {
6561
+ app.log.warn({
6562
+ err: parsed.error.flatten(),
6563
+ agentId: agent.uuid,
6564
+ clientId: agent.clientId
6565
+ }, "agent:pinned frame failed schema validation — not sending");
6566
+ return;
6567
+ }
6568
+ sendToClient(agent.clientId, parsed.data);
6569
+ }
6353
6570
  const listAgentsFilterSchema = z.object({ type: agentTypeSchema$1.optional() });
6354
6571
  app.get("/", async (request) => {
6355
6572
  const query = paginationQuerySchema.parse(request.query);
@@ -6380,6 +6597,7 @@ async function adminAgentRoutes(app) {
6380
6597
  source: body.source ?? "admin-api",
6381
6598
  managerId
6382
6599
  });
6600
+ notifyClientAgentPinned(agent);
6383
6601
  return reply.status(201).send({
6384
6602
  ...agent,
6385
6603
  createdAt: agent.createdAt.toISOString(),
@@ -6392,7 +6610,9 @@ async function adminAgentRoutes(app) {
6392
6610
  const body = updateAgentSchema.parse(request.body);
6393
6611
  const member = requireMember(request);
6394
6612
  if (body.managerId !== void 0 && member.role !== "admin") throw new ForbiddenError("Only admins can reassign an agent's manager");
6613
+ const before = body.clientId !== void 0 ? await getAgent(app.db, request.params.uuid) : null;
6395
6614
  const agent = await updateAgent(app.db, request.params.uuid, body);
6615
+ if (before && before.clientId === null && agent.clientId !== null) notifyClientAgentPinned(agent);
6396
6616
  return {
6397
6617
  ...agent,
6398
6618
  createdAt: agent.createdAt.toISOString(),
@@ -8785,6 +9005,32 @@ function clientWsRoutes(notifier, instanceId) {
8785
9005
  type: "client:registered",
8786
9006
  clientId: data.clientId
8787
9007
  }));
9008
+ try {
9009
+ const pinned = await listActiveAgentsPinnedToClient(app.db, data.clientId);
9010
+ for (const agent of pinned) {
9011
+ const parsed = agentPinnedMessageSchema$1.safeParse({
9012
+ type: "agent:pinned",
9013
+ agentId: agent.uuid,
9014
+ name: agent.name,
9015
+ displayName: agent.displayName,
9016
+ agentType: agent.type
9017
+ });
9018
+ if (!parsed.success) {
9019
+ app.log.warn({
9020
+ err: parsed.error.flatten(),
9021
+ agentId: agent.uuid,
9022
+ clientId: data.clientId
9023
+ }, "agent:pinned backfill frame failed schema validation — skipping");
9024
+ continue;
9025
+ }
9026
+ socket.send(JSON.stringify(parsed.data));
9027
+ }
9028
+ } catch (err) {
9029
+ app.log.error({
9030
+ err,
9031
+ clientId: data.clientId
9032
+ }, "agent:pinned backfill on client:register failed — client may need manual `agent add`");
9033
+ }
8788
9034
  } else if (type === "agent:bind") {
8789
9035
  if (!clientId) {
8790
9036
  socket.send(JSON.stringify({
@@ -199,6 +199,19 @@ z.object({
199
199
  branch: z.string().nullable()
200
200
  });
201
201
  /**
202
+ * Server → client WebSocket frame announcing that an agent has just been
203
+ * pinned to the connected client (either created with `clientId` or bound via
204
+ * PATCH NULL → ID). The client can auto-register a local config from this so
205
+ * the operator doesn't have to run `first-tree-hub agent add` manually.
206
+ */
207
+ const agentPinnedMessageSchema = z.object({
208
+ type: z.literal("agent:pinned"),
209
+ agentId: z.string(),
210
+ name: z.string().nullable(),
211
+ displayName: z.string().nullable(),
212
+ agentType: agentTypeSchema
213
+ });
214
+ /**
202
215
  * Agent runtime configuration — M1 (Claude Code only).
203
216
  *
204
217
  * Defines the 5 user-tunable field groups that the Hub centrally manages
@@ -877,4 +890,4 @@ async function bindFeishuUser(serverUrl, accessToken, agentId, humanAgentId, fei
877
890
  }
878
891
  }
879
892
  //#endregion
880
- export { updateAgentSchema as $, createOrganizationSchema as A, paginationQuerySchema as B, clientRegisterSchema as C, createAgentSchema as D, createAdapterMappingSchema as E, isRedactedEnvValue as F, sendToAgentSchema as G, runtimeStateMessageSchema as H, linkTaskChatSchema as I, sessionEventSchema as J, sessionCompletionMessageSchema as K, loginSchema as L, delegateFeishuUserSchema as M, dryRunAgentRuntimeConfigSchema as N, createChatSchema as O, inboxPollQuerySchema as P, updateAgentRuntimeConfigSchema as Q, messageSourceSchema as R, agentTypeSchema as S, createAdapterConfigSchema as T, selfServiceFeishuBotSchema as U, refreshTokenSchema as V, sendMessageSchema as W, taskListQuerySchema as X, sessionStateMessageSchema as Y, updateAdapterConfigSchema as Z, addParticipantSchema as _, AGENT_SELECTOR_HEADER as a, agentBindRequestSchema as b, AGENT_TYPES as c, SYSTEM_CONFIG_DEFAULTS as d, updateMemberSchema as et, TASK_CREATOR_TYPES as f, WS_AUTH_FRAME_TIMEOUT_MS as g, TASK_TERMINAL_STATUSES as h, AGENT_BIND_REJECT_REASONS as i, wsAuthFrameSchema as it, createTaskSchema as j, createMemberSchema as k, AGENT_VISIBILITY as l, TASK_STATUSES as m, bindFeishuUser as n, updateSystemConfigSchema as nt, AGENT_SOURCES as o, TASK_HEALTH_SIGNALS as p, sessionEventMessageSchema as q, feishu_exports as r, updateTaskStatusSchema as rt, AGENT_STATUSES as s, bindFeishuBot as t, updateOrganizationSchema as tt, DEFAULT_AGENT_RUNTIME_CONFIG_PAYLOAD as u, adminCreateTaskSchema as v, connectTokenExchangeSchema as w, agentRuntimeConfigPayloadSchema as x, adminUpdateTaskSchema as y, notificationQuerySchema as z };
893
+ export { updateAgentRuntimeConfigSchema as $, createMemberSchema as A, notificationQuerySchema as B, agentTypeSchema as C, createAdapterMappingSchema as D, createAdapterConfigSchema as E, inboxPollQuerySchema as F, sendMessageSchema as G, refreshTokenSchema as H, isRedactedEnvValue as I, sessionEventMessageSchema as J, sendToAgentSchema as K, linkTaskChatSchema as L, createTaskSchema as M, delegateFeishuUserSchema as N, createAgentSchema as O, dryRunAgentRuntimeConfigSchema as P, updateAdapterConfigSchema as Q, loginSchema as R, agentRuntimeConfigPayloadSchema as S, connectTokenExchangeSchema as T, runtimeStateMessageSchema as U, paginationQuerySchema as V, selfServiceFeishuBotSchema as W, sessionStateMessageSchema as X, sessionEventSchema as Y, taskListQuerySchema as Z, addParticipantSchema as _, AGENT_SELECTOR_HEADER as a, wsAuthFrameSchema as at, agentBindRequestSchema as b, AGENT_TYPES as c, SYSTEM_CONFIG_DEFAULTS as d, updateAgentSchema as et, TASK_CREATOR_TYPES as f, WS_AUTH_FRAME_TIMEOUT_MS as g, TASK_TERMINAL_STATUSES as h, AGENT_BIND_REJECT_REASONS as i, updateTaskStatusSchema as it, createOrganizationSchema as j, createChatSchema as k, AGENT_VISIBILITY as l, TASK_STATUSES as m, bindFeishuUser as n, updateOrganizationSchema as nt, AGENT_SOURCES as o, TASK_HEALTH_SIGNALS as p, sessionCompletionMessageSchema as q, feishu_exports as r, updateSystemConfigSchema as rt, AGENT_STATUSES as s, bindFeishuBot as t, updateMemberSchema as tt, DEFAULT_AGENT_RUNTIME_CONFIG_PAYLOAD as u, adminCreateTaskSchema as v, clientRegisterSchema as w, agentPinnedMessageSchema as x, adminUpdateTaskSchema as y, messageSourceSchema as z };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as resolveAccessToken, n as ensureFreshAccessToken, o as resolveServerUrl, r as ensureFreshAdminToken } from "./bootstrap-CRDR6NwE.mjs";
2
- import { A as stopPostgres, C as checkServerReachable, D as status, E as blank, F as SdkError, M as createOwner, N as hasUser, O as ensurePostgres, P as FirstTreeHubSDK, S as checkServerHealth, T as printResults, _ as checkClientConfig, a as uninstallClientService, b as checkNodeVersion, c as promptAddAgent, f as onboardCheck, g as checkAgentConfigs, h as runMigrations, i as resolveCliInvocation, j as ClientRuntime, k as isDockerAvailable, l as promptMissingFields, 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-6-paFwyo.mjs";
3
- import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-CJ08ntOD.mjs";
1
+ import { a as resolveAccessToken, n as ensureFreshAccessToken, o as resolveServerUrl, r as ensureFreshAdminToken } from "./bootstrap-8nCntTrK.mjs";
2
+ import { A as stopPostgres, C as checkServerReachable, D as status, E as blank, F as SdkError, M as createOwner, N as hasUser, O as ensurePostgres, P as FirstTreeHubSDK, S as checkServerHealth, T as printResults, _ as checkClientConfig, a as uninstallClientService, b as checkNodeVersion, c as promptAddAgent, f as onboardCheck, g as checkAgentConfigs, h as runMigrations, i as resolveCliInvocation, j as ClientRuntime, k as isDockerAvailable, l as promptMissingFields, 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-BA5U1v9L.mjs";
3
+ import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-D9JkMZnU.mjs";
4
4
  export { ClientRuntime, FirstTreeHubSDK, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, getClientServiceStatus, hasUser, installClientService, isDockerAvailable, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, runMigrations, startServer, status, stopPostgres, uninstallClientService };