@agent-team-foundation/first-tree-hub 0.10.8 → 0.10.9

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,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import "../observability-DPyf745N-BSc8QNcR.mjs";
3
- import { A as checkServerHealth, C as checkAgentConfigs, D as checkDocker, E as checkDatabase, F as installClientService, G as createOwner, H as ClientRuntime, I as isServiceSupported, J as fail, M as checkWebSocket, N as printResults, O as checkNodeVersion, P as getClientServiceStatus, Q as setJsonMode, S as runMigrations, T as checkClientConfig, U as handleClientOrgMismatch, V as stopPostgres, Y as success, Z as print, _ as onboardCreate, a as declineUpdate, at as cleanWorkspaces, b as createApiNameResolver, c as COMMAND_VERSION, ct as configureClientLoggerForService, d as isInteractive, et as ClientOrgMismatchError, f as promptAddAgent, g as onboardCheck, h as loadOnboardState, i as createExecuteUpdate, it as SessionRegistry, j as checkServerReachable, k as checkServerConfig, l as reconcileLocalRuntimeProviders, m as formatCheckReport, nt as FirstTreeHubSDK, o as promptUpdate, ot as probeCapabilities, p as promptMissingFields, q as resolveReplyToFromEnv, r as registerSaaSConnectCommand, rt as SdkError, s as startServer, st as applyClientLoggerConfig, tt as ClientUserMismatchError, u as uploadClientCapabilities, v as saveOnboardState, w as checkBackgroundService, x as migrateLocalAgentDirs, y as runHomeMigration } from "../saas-connect-D-fy3xu-.mjs";
3
+ import { $ as success, A as checkServerHealth, C as checkAgentConfigs, D as checkDocker, E as checkDatabase, F as getClientServiceStatus, H as stopPostgres, I as installClientService, J as removeLocalAgent, K as findStaleAliases, L as isServiceSupported, M as checkWebSocket, N as printResults, O as checkNodeVersion, P as reconcileAgentConfigs, Q as fail, S as runMigrations, T as checkClientConfig, U as ClientRuntime, W as handleClientOrgMismatch, Y as createOwner, Z as resolveReplyToFromEnv, _ as onboardCreate, a as declineUpdate, at as ClientUserMismatchError, b as createApiNameResolver, c as COMMAND_VERSION, ct as SessionRegistry, d as isInteractive, dt as applyClientLoggerConfig, f as promptAddAgent, ft as configureClientLoggerForService, g as onboardCheck, h as loadOnboardState, i as createExecuteUpdate, it as ClientOrgMismatchError, j as checkServerReachable, k as checkServerConfig, l as reconcileLocalRuntimeProviders, lt as cleanWorkspaces, m as formatCheckReport, nt as setJsonMode, o as promptUpdate, ot as FirstTreeHubSDK, p as promptMissingFields, q as formatStaleReason, r as registerSaaSConnectCommand, s as startServer, st as SdkError, tt as print, u as uploadClientCapabilities, ut as probeCapabilities, v as saveOnboardState, w as checkBackgroundService, x as migrateLocalAgentDirs, y as runHomeMigration } from "../saas-connect-CKQ15VLz.mjs";
4
4
  import "../logger-core-BTmvdflj-DjW8FM4T.mjs";
5
- import { C as serverConfigSchema, _ as loadAgents, b as resetConfig, c as saveCredentials, d as DEFAULT_HOME_DIR, 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-jx5nN1qZ.mjs";
5
+ import { C as serverConfigSchema, S as resolveConfigReadonly, _ as loadAgents, b as resetConfig, c as saveCredentials, d as DEFAULT_HOME_DIR, 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-jx5nN1qZ.mjs";
6
6
  import "../dist-DSr_I5Ia.mjs";
7
7
  import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-eynC54km.mjs";
8
8
  import "../invitation-B1pjAyOz-BaCA9PII.mjs";
9
9
  import { join } from "node:path";
10
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
10
+ import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
11
11
  import { Command } from "commander";
12
12
  import { confirm, input, password, select } from "@inquirer/prompts";
13
13
  //#region src/commands/agent-config.ts
@@ -281,6 +281,21 @@ async function resolveAgent(serverUrl, adminToken, agentName) {
281
281
  if (!found) fail("NOT_FOUND", `Agent "${agentName}" not found`, 1);
282
282
  return found;
283
283
  }
284
+ /**
285
+ * Read the persisted `client.id` from `client.yaml`. Required by `agent
286
+ * prune` to filter the user-scoped `listMyAgents` response down to "what
287
+ * actually binds on THIS machine". `fail()` instead of throwing so the
288
+ * "no client.yaml — run client connect first" path renders as a clean
289
+ * CLI error rather than a stack trace.
290
+ */
291
+ function readClientId() {
292
+ const id = resolveConfigReadonly({
293
+ schema: clientConfigSchema,
294
+ role: "client"
295
+ }).client?.id;
296
+ if (typeof id !== "string" || id.length === 0) fail("MISSING_CLIENT_ID", "No client.id found in client.yaml. Run `first-tree-hub connect <token>` first.", 2);
297
+ return id;
298
+ }
284
299
  function registerAgentCommands(program) {
285
300
  const agent = program.command("agent").description("Agent management — config, bindings, messaging");
286
301
  registerAgentConfigCommands(agent);
@@ -307,22 +322,65 @@ function registerAgentCommands(program) {
307
322
  }
308
323
  });
309
324
  agent.command("remove <name>").description("Remove an agent from this client and delete its local runtime data (config dir, workspace, session state)").action((name) => {
310
- const agentDir = join(DEFAULT_CONFIG_DIR, "agents", name);
311
- if (!existsSync(agentDir)) {
325
+ if (!existsSync(join(DEFAULT_CONFIG_DIR, "agents", name))) {
312
326
  print.line(` Agent "${name}" not found.\n`);
313
327
  process.exit(1);
314
328
  }
315
- rmSync(agentDir, {
316
- recursive: true,
317
- force: true
318
- });
319
- rmSync(join(DEFAULT_DATA_DIR, "workspaces", name), {
320
- recursive: true,
321
- force: true
322
- });
323
- rmSync(join(DEFAULT_DATA_DIR, "sessions", `${name}.json`), { force: true });
329
+ removeLocalAgent(name);
324
330
  print.line(` Agent "${name}" removed.\n`);
325
331
  });
332
+ agent.command("prune").description("Remove local agent aliases that won't bind on this client (unowned, pinned elsewhere, or unreadable)").option("--yes", "Skip the interactive confirmation prompt").option("--dry-run", "Only list what would be removed; don't touch the filesystem").option("--server <url>", "Hub server URL").action(async (options) => {
333
+ try {
334
+ const serverUrl = resolveServerUrl(options.server);
335
+ const clientId = readClientId();
336
+ const sdk = new FirstTreeHubSDK({
337
+ serverUrl,
338
+ getAccessToken: () => ensureFreshAccessToken()
339
+ });
340
+ const stale = await findStaleAliases({
341
+ clientId,
342
+ listPinnedAgents: () => sdk.listMyAgents()
343
+ });
344
+ if (stale.length === 0) {
345
+ print.line("\n ✓ No stale agent aliases. Local config matches the server.\n\n");
346
+ return;
347
+ }
348
+ print.line(`\n ${stale.length} stale ${stale.length === 1 ? "alias" : "aliases"}:\n\n`);
349
+ for (const s of stale) {
350
+ const id = s.agentId ?? "—";
351
+ print.line(` - ${s.name.padEnd(30)} ${id.padEnd(38)} ${formatStaleReason(s.reason)}\n`);
352
+ }
353
+ print.line("\n");
354
+ if (options.dryRun) {
355
+ print.line(" Dry run — no files removed. Re-run without --dry-run to delete.\n\n");
356
+ return;
357
+ }
358
+ if (!options.yes) {
359
+ if (!await confirm({
360
+ message: `Remove the ${stale.length} stale ${stale.length === 1 ? "alias" : "aliases"} above (config + workspace + session state)?`,
361
+ default: false
362
+ }).catch(() => false)) {
363
+ print.line(" Cancelled.\n\n");
364
+ return;
365
+ }
366
+ }
367
+ let removed = 0;
368
+ let failed = 0;
369
+ for (const s of stale) try {
370
+ removeLocalAgent(s.name);
371
+ print.line(` ✓ removed ${s.name}\n`);
372
+ removed++;
373
+ } catch (err) {
374
+ const msg = err instanceof Error ? err.message : String(err);
375
+ print.line(` ✗ ${s.name} (${msg.slice(0, 80)})\n`);
376
+ failed++;
377
+ }
378
+ print.line(`\n ${removed} pruned${failed > 0 ? `, ${failed} failed (re-run to retry)` : ""}.\n\n`);
379
+ if (failed > 0) process.exitCode = 1;
380
+ } catch (error) {
381
+ fail("PRUNE_ERROR", error instanceof Error ? error.message : String(error));
382
+ }
383
+ });
326
384
  agent.command("list").description("List agents — locally-configured by default, or every agent you manage with --remote").option("--remote", "List every agent you manage on the Hub server (cross-org)").option("--org <id>", "When listing remote, restrict to a single organization id").option("--server <url>", "Hub server URL").action(async (options) => {
327
385
  if (!(options.remote === true || typeof options.org === "string")) {
328
386
  const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
@@ -1124,11 +1182,32 @@ function registerClientCommands(program) {
1124
1182
  });
1125
1183
  client.command("doctor").description("Check client environment readiness").action(async () => {
1126
1184
  print.line("\n First Tree Hub Client Doctor\n\n");
1185
+ let agentCheck;
1186
+ try {
1187
+ const serverUrl = resolveServerUrl();
1188
+ const cfg = await initConfig({
1189
+ schema: clientConfigSchema,
1190
+ role: "client"
1191
+ });
1192
+ const sdk = new FirstTreeHubSDK({
1193
+ serverUrl,
1194
+ getAccessToken: () => ensureFreshAccessToken()
1195
+ });
1196
+ agentCheck = await reconcileAgentConfigs({
1197
+ clientId: cfg.client.id,
1198
+ listPinnedAgents: () => sdk.listMyAgents()
1199
+ });
1200
+ } catch {
1201
+ agentCheck = checkAgentConfigs();
1202
+ } finally {
1203
+ resetConfig();
1204
+ resetConfigMeta();
1205
+ }
1127
1206
  printResults([
1128
1207
  checkNodeVersion(),
1129
1208
  checkClientConfig(),
1130
1209
  await checkServerReachable(),
1131
- checkAgentConfigs(),
1210
+ agentCheck,
1132
1211
  await checkWebSocket(),
1133
1212
  checkBackgroundService()
1134
1213
  ]);
@@ -1182,7 +1261,7 @@ function registerClientCommands(program) {
1182
1261
  fail("CLIENT_LIST_ERROR", error instanceof Error ? error.message : String(error));
1183
1262
  }
1184
1263
  });
1185
- client.command("claim").description("Transfer ownership of this machine to your account (unpins the previous owner's agents from this machine)").option("--confirm", "Skip the interactive confirmation prompt").option("--server <url>", "Hub server URL").action(async (options) => {
1264
+ client.command("claim").description("Transfer ownership of this machine to your account (unpins the previous owner's agents from this machine)").option("--confirm", "Skip confirmation prompts (claim + auto-prune stale aliases)").option("--server <url>", "Hub server URL").action(async (options) => {
1186
1265
  try {
1187
1266
  const config = await initConfig({
1188
1267
  schema: clientConfigSchema,
@@ -1221,7 +1300,47 @@ function registerClientCommands(program) {
1221
1300
  }
1222
1301
  const result = await response.json();
1223
1302
  print.line(` ✓ Ownership transferred. ${result.unpinnedAgentCount} agent(s) unpinned.\n`);
1224
- print.line(" Run `first-tree-hub client start` to reconnect.\n\n");
1303
+ try {
1304
+ const sdk = new FirstTreeHubSDK({
1305
+ serverUrl,
1306
+ getAccessToken: () => ensureFreshAccessToken()
1307
+ });
1308
+ const stale = await findStaleAliases({
1309
+ clientId,
1310
+ listPinnedAgents: () => sdk.listMyAgents()
1311
+ });
1312
+ if (stale.length === 0) print.line(" No stale local aliases — local config already matches the server.\n");
1313
+ else {
1314
+ print.line(`\n ${stale.length} local ${stale.length === 1 ? "alias" : "aliases"} won't bind on this client:\n\n`);
1315
+ for (const s of stale) {
1316
+ const id = s.agentId ?? "—";
1317
+ print.line(` - ${s.name.padEnd(30)} ${id.padEnd(38)} ${formatStaleReason(s.reason)}\n`);
1318
+ }
1319
+ print.line("\n");
1320
+ if (options.confirm === true ? true : await confirm({
1321
+ message: `Remove the ${stale.length} stale ${stale.length === 1 ? "alias" : "aliases"} above (config + workspace + session state)?`,
1322
+ default: true
1323
+ }).catch(() => false)) {
1324
+ let removed = 0;
1325
+ let failed = 0;
1326
+ for (const s of stale) try {
1327
+ removeLocalAgent(s.name);
1328
+ print.line(` ✓ removed ${s.name}\n`);
1329
+ removed++;
1330
+ } catch (err) {
1331
+ const msg = err instanceof Error ? err.message : String(err);
1332
+ print.line(` ✗ ${s.name} (${msg.slice(0, 80)})\n`);
1333
+ failed++;
1334
+ }
1335
+ print.line(`\n ${removed} pruned${failed > 0 ? `, ${failed} failed (re-run \`agent prune\` to retry)` : ""}.\n`);
1336
+ } else print.line(" Skipped. Run `first-tree-hub agent prune` later to clean up.\n");
1337
+ }
1338
+ } catch (err) {
1339
+ const msg = err instanceof Error ? err.message : String(err);
1340
+ print.line(` (Could not check for stale aliases: ${msg.slice(0, 100)})\n`);
1341
+ print.line(" Run `first-tree-hub agent prune` after reconnecting.\n");
1342
+ }
1343
+ print.line("\n Run `first-tree-hub client start` to reconnect.\n\n");
1225
1344
  } catch (error) {
1226
1345
  fail("CLAIM_ERROR", error instanceof Error ? error.message : String(error));
1227
1346
  } finally {
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import "./observability-DPyf745N-BSc8QNcR.mjs";
2
- import { $ as status, A as checkServerHealth, B as isDockerAvailable, C as checkAgentConfigs, D as checkDocker, E as checkDatabase, F as installClientService, G as createOwner, H as ClientRuntime, I as isServiceSupported, K as hasUser, L as resolveCliInvocation, M as checkWebSocket, N as printResults, O as checkNodeVersion, P as getClientServiceStatus, R as uninstallClientService, S as runMigrations, T as checkClientConfig, U as handleClientOrgMismatch, V as stopPostgres, W as rotateClientIdWithBackup, X as blank, _ as onboardCreate, d as isInteractive, f as promptAddAgent, g as onboardCheck, j as checkServerReachable, k as checkServerConfig, m as formatCheckReport, n as deriveHubUrlFromToken, nt as FirstTreeHubSDK, p as promptMissingFields, rt as SdkError, s as startServer, t as HubUrlDerivationError, y as runHomeMigration, z as ensurePostgres } from "./saas-connect-D-fy3xu-.mjs";
2
+ import { A as checkServerHealth, B as ensurePostgres, C as checkAgentConfigs, D as checkDocker, E as checkDatabase, F as getClientServiceStatus, G as rotateClientIdWithBackup, H as stopPostgres, I as installClientService, L as isServiceSupported, M as checkWebSocket, N as printResults, O as checkNodeVersion, R as resolveCliInvocation, S as runMigrations, T as checkClientConfig, U as ClientRuntime, V as isDockerAvailable, W as handleClientOrgMismatch, X as hasUser, Y as createOwner, _ as onboardCreate, d as isInteractive, et as blank, f as promptAddAgent, g as onboardCheck, j as checkServerReachable, k as checkServerConfig, m as formatCheckReport, n as deriveHubUrlFromToken, ot as FirstTreeHubSDK, p as promptMissingFields, rt as status, s as startServer, st as SdkError, t as HubUrlDerivationError, y as runHomeMigration, z as uninstallClientService } from "./saas-connect-CKQ15VLz.mjs";
3
3
  import "./logger-core-BTmvdflj-DjW8FM4T.mjs";
4
4
  import { a as resolveAccessToken, n as ensureFreshAccessToken, o as resolveServerUrl, r as ensureFreshAdminToken } from "./bootstrap-jx5nN1qZ.mjs";
5
5
  import "./dist-DSr_I5Ia.mjs";
@@ -19,11 +19,11 @@ import { execFileSync, execSync, spawn, spawnSync } from "node:child_process";
19
19
  import { Codex } from "@openai/codex-sdk";
20
20
  import { fileURLToPath } from "node:url";
21
21
  import * as semver from "semver";
22
+ import { confirm, input, password, select } from "@inquirer/prompts";
22
23
  import bcrypt from "bcrypt";
23
24
  import { and, asc, count, desc, eq, gt, inArray, isNotNull, isNull, lt, ne, or, sql } from "drizzle-orm";
24
25
  import { drizzle } from "drizzle-orm/postgres-js";
25
26
  import postgres from "postgres";
26
- import { confirm, input, password, select } from "@inquirer/prompts";
27
27
  import { migrate } from "drizzle-orm/postgres-js/migrator";
28
28
  import { bigserial, boolean, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique, uniqueIndex } from "drizzle-orm/pg-core";
29
29
  import cors from "@fastify/cors";
@@ -5196,6 +5196,12 @@ var AgentSlot = class {
5196
5196
  agentId: config.agentId
5197
5197
  });
5198
5198
  }
5199
+ get name() {
5200
+ return this.config.name;
5201
+ }
5202
+ get agentId() {
5203
+ return this.config.agentId;
5204
+ }
5199
5205
  get clientConnection() {
5200
5206
  return this.config.clientConnection;
5201
5207
  }
@@ -5998,6 +6004,130 @@ function makeUuidV7() {
5998
6004
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
5999
6005
  }
6000
6006
  //#endregion
6007
+ //#region src/core/agent-prune.ts
6008
+ const minimalAgentYamlSchema = z.object({ agentId: z.string().min(1) }).passthrough();
6009
+ /**
6010
+ * Cross-reference local `agents/<name>/agent.yaml` files against the
6011
+ * server's pinned-agent set, returning every alias that won't bind on
6012
+ * THIS client.
6013
+ *
6014
+ * Why we don't use `loadAgents`:
6015
+ * `shared/config/loader.loadAgents` is fail-fast — one malformed
6016
+ * agent.yaml throws and the whole scan dies. The dominant prune target
6017
+ * IS the malformed dir (typo `agent add d`, half-written yaml, missing
6018
+ * agentId), so we walk dirs ourselves and degrade per-entry instead.
6019
+ *
6020
+ * Why we filter by clientId, not just userId:
6021
+ * `listPinnedAgents` (`/api/v1/clients/me/agents`) returns every agent
6022
+ * pinned to ANY client this user owns (cross-machine). For prune the
6023
+ * relevant question is "will R-RUN accept it on THIS machine", which
6024
+ * needs `agents.client_id === current client.id`. Anything pinned on
6025
+ * another client is reported with `pinned-elsewhere` so the operator
6026
+ * can either re-pin or delete the local alias deliberately.
6027
+ */
6028
+ async function findStaleAliases(opts) {
6029
+ const agentsDir = opts.agentsDir ?? join(DEFAULT_CONFIG_DIR, "agents");
6030
+ if (!existsSync(agentsDir)) return [];
6031
+ const remote = await opts.listPinnedAgents();
6032
+ const pinnedHere = /* @__PURE__ */ new Set();
6033
+ const pinnedElsewhere = /* @__PURE__ */ new Map();
6034
+ for (const r of remote) if (r.clientId === opts.clientId) pinnedHere.add(r.agentId);
6035
+ else pinnedElsewhere.set(r.agentId, r.clientId);
6036
+ const stale = [];
6037
+ for (const entry of readdirSync(agentsDir)) {
6038
+ const agentDir = join(agentsDir, entry);
6039
+ let isDir = false;
6040
+ try {
6041
+ isDir = statSync(agentDir).isDirectory();
6042
+ } catch {
6043
+ continue;
6044
+ }
6045
+ if (!isDir) continue;
6046
+ const yamlPath = join(agentDir, "agent.yaml");
6047
+ if (!existsSync(yamlPath)) {
6048
+ stale.push({
6049
+ name: entry,
6050
+ agentId: null,
6051
+ reason: {
6052
+ kind: "unreadable",
6053
+ error: "missing agent.yaml"
6054
+ }
6055
+ });
6056
+ continue;
6057
+ }
6058
+ let agentId;
6059
+ try {
6060
+ const raw = parse(readFileSync(yamlPath, "utf-8"));
6061
+ const parsed = minimalAgentYamlSchema.safeParse(raw);
6062
+ if (!parsed.success) {
6063
+ const issue = parsed.error.issues[0]?.message ?? "schema error";
6064
+ stale.push({
6065
+ name: entry,
6066
+ agentId: null,
6067
+ reason: {
6068
+ kind: "unreadable",
6069
+ error: issue
6070
+ }
6071
+ });
6072
+ continue;
6073
+ }
6074
+ agentId = parsed.data.agentId;
6075
+ } catch (err) {
6076
+ const msg = err instanceof Error ? err.message : String(err);
6077
+ stale.push({
6078
+ name: entry,
6079
+ agentId: null,
6080
+ reason: {
6081
+ kind: "unreadable",
6082
+ error: msg
6083
+ }
6084
+ });
6085
+ continue;
6086
+ }
6087
+ if (pinnedHere.has(agentId)) continue;
6088
+ const otherClient = pinnedElsewhere.get(agentId);
6089
+ if (otherClient !== void 0) stale.push({
6090
+ name: entry,
6091
+ agentId,
6092
+ reason: {
6093
+ kind: "pinned-elsewhere",
6094
+ clientId: otherClient
6095
+ }
6096
+ });
6097
+ else stale.push({
6098
+ name: entry,
6099
+ agentId,
6100
+ reason: { kind: "unowned" }
6101
+ });
6102
+ }
6103
+ return stale;
6104
+ }
6105
+ /** Human-readable suffix for the per-alias listing. */
6106
+ function formatStaleReason(reason) {
6107
+ switch (reason.kind) {
6108
+ case "unreadable": return `unreadable: ${reason.error}`;
6109
+ case "unowned": return "no longer owned by you (deleted or transferred)";
6110
+ case "pinned-elsewhere": return `pinned to another client: ${reason.clientId}`;
6111
+ }
6112
+ }
6113
+ /**
6114
+ * Remove an agent's local footprint: the YAML alias dir, the workspace
6115
+ * tree under `data/workspaces/<name>`, and the session-mapping file under
6116
+ * `data/sessions/<name>.json`. Mirrors what `agent remove` does, exposed
6117
+ * separately so prune and claim can share it.
6118
+ */
6119
+ function removeLocalAgent(name) {
6120
+ rmSync(join(DEFAULT_CONFIG_DIR, "agents", name), {
6121
+ recursive: true,
6122
+ force: true
6123
+ });
6124
+ rmSync(join(DEFAULT_DATA_DIR$1, "workspaces", name), {
6125
+ recursive: true,
6126
+ force: true
6127
+ });
6128
+ rmSync(join(DEFAULT_DATA_DIR$1, "sessions", `${name}.json`), { force: true });
6129
+ }
6130
+ //#endregion
6001
6131
  //#region src/core/client-reidentify.ts
6002
6132
  /**
6003
6133
  * Handle a `CLIENT_ORG_MISMATCH` from the server by rotating the local
@@ -7090,6 +7220,69 @@ function checkAgentConfigs() {
7090
7220
  };
7091
7221
  }
7092
7222
  }
7223
+ /**
7224
+ * Server-aware agent reconciliation. Walks `agents/<name>/agent.yaml` and
7225
+ * cross-references each `agentId` with `/api/v1/clients/me/agents`,
7226
+ * filtering by `clientId` so the "stale" verdict matches what R-RUN will
7227
+ * actually accept on this machine.
7228
+ *
7229
+ * Categorises each local alias into:
7230
+ * - pinned — bind would succeed on this client.
7231
+ * - pinned-elsewhere — owned by you, but pinned to a different client
7232
+ * (alias is dead weight here; real agent is alive
7233
+ * on the other machine).
7234
+ * - unowned — agentId not in the server's response at all.
7235
+ * - unreadable — yaml missing/malformed/no agentId.
7236
+ *
7237
+ * The plain `checkAgentConfigs` (sync, local-only) is retained for
7238
+ * back-compat with external consumers but its "N configured" wording is
7239
+ * misleading because stale aliases never bind at runtime.
7240
+ *
7241
+ * Skipped reconciliation (server unreachable / unauthenticated) returns
7242
+ * `ok: false` — doctor's other server-touching checks already report
7243
+ * connectivity loss as a failure, and silently passing here would hide
7244
+ * the very issue the operator is running doctor to diagnose.
7245
+ */
7246
+ async function reconcileAgentConfigs(opts) {
7247
+ const agentsDir = opts.agentsDir ?? join(DEFAULT_CONFIG_DIR, "agents");
7248
+ let localCount = 0;
7249
+ if (existsSync(agentsDir)) for (const entry of readdirSync(agentsDir)) try {
7250
+ if (statSync(join(agentsDir, entry)).isDirectory()) localCount++;
7251
+ } catch {}
7252
+ if (localCount === 0) return {
7253
+ label: "Agents",
7254
+ ok: false,
7255
+ detail: "no agents configured"
7256
+ };
7257
+ let stale;
7258
+ try {
7259
+ stale = await findStaleAliases({
7260
+ clientId: opts.clientId,
7261
+ listPinnedAgents: opts.listPinnedAgents,
7262
+ agentsDir
7263
+ });
7264
+ } catch (err) {
7265
+ const msg = err instanceof Error ? err.message : String(err);
7266
+ return {
7267
+ label: "Agents",
7268
+ ok: false,
7269
+ detail: `${localCount} configured locally — server reconciliation failed (${msg.slice(0, 60)})`
7270
+ };
7271
+ }
7272
+ const pinnedCount = localCount - stale.length;
7273
+ if (stale.length === 0) return {
7274
+ label: "Agents",
7275
+ ok: true,
7276
+ detail: `${localCount} configured, all pinned to this client`
7277
+ };
7278
+ const staleSummary = stale.map((s) => `${s.name} [${formatStaleReason(s.reason)}]`).slice(0, 5).join("; ");
7279
+ const truncated = stale.length > 5 ? `; ...+${stale.length - 5} more` : "";
7280
+ return {
7281
+ label: "Agents",
7282
+ ok: false,
7283
+ detail: `${localCount} configured locally, ${pinnedCount} pinned to this client; ${stale.length} stale: ${staleSummary}${truncated} — run \`first-tree-hub agent prune\` to clean up`
7284
+ };
7285
+ }
7093
7286
  function checkBackgroundService() {
7094
7287
  const info = getClientServiceStatus();
7095
7288
  if (info.platform === "unsupported") return {
@@ -18560,4 +18753,4 @@ function registerSaaSConnectCommand(program) {
18560
18753
  });
18561
18754
  }
18562
18755
  //#endregion
18563
- export { status as $, checkServerHealth as A, isDockerAvailable as B, checkAgentConfigs as C, checkDocker as D, checkDatabase as E, installClientService as F, createOwner as G, ClientRuntime as H, isServiceSupported as I, fail as J, hasUser as K, resolveCliInvocation as L, checkWebSocket as M, printResults as N, checkNodeVersion as O, getClientServiceStatus as P, setJsonMode as Q, uninstallClientService as R, runMigrations as S, checkClientConfig as T, handleClientOrgMismatch as U, stopPostgres as V, rotateClientIdWithBackup as W, blank as X, success as Y, print as Z, onboardCreate as _, declineUpdate as a, cleanWorkspaces as at, createApiNameResolver as b, COMMAND_VERSION as c, configureClientLoggerForService as ct, isInteractive as d, ClientOrgMismatchError as et, promptAddAgent as f, onboardCheck as g, loadOnboardState as h, createExecuteUpdate as i, SessionRegistry as it, checkServerReachable as j, checkServerConfig as k, reconcileLocalRuntimeProviders as l, formatCheckReport as m, deriveHubUrlFromToken as n, FirstTreeHubSDK as nt, promptUpdate as o, probeCapabilities as ot, promptMissingFields as p, resolveReplyToFromEnv as q, registerSaaSConnectCommand as r, SdkError as rt, startServer as s, applyClientLoggerConfig as st, HubUrlDerivationError as t, ClientUserMismatchError as tt, uploadClientCapabilities as u, saveOnboardState as v, checkBackgroundService as w, migrateLocalAgentDirs as x, runHomeMigration as y, ensurePostgres as z };
18756
+ export { success as $, checkServerHealth as A, ensurePostgres as B, checkAgentConfigs as C, checkDocker as D, checkDatabase as E, getClientServiceStatus as F, rotateClientIdWithBackup as G, stopPostgres as H, installClientService as I, removeLocalAgent as J, findStaleAliases as K, isServiceSupported as L, checkWebSocket as M, printResults as N, checkNodeVersion as O, reconcileAgentConfigs as P, fail as Q, resolveCliInvocation as R, runMigrations as S, checkClientConfig as T, ClientRuntime as U, isDockerAvailable as V, handleClientOrgMismatch as W, hasUser as X, createOwner as Y, resolveReplyToFromEnv as Z, onboardCreate as _, declineUpdate as a, ClientUserMismatchError as at, createApiNameResolver as b, COMMAND_VERSION as c, SessionRegistry as ct, isInteractive as d, applyClientLoggerConfig as dt, blank as et, promptAddAgent as f, configureClientLoggerForService as ft, onboardCheck as g, loadOnboardState as h, createExecuteUpdate as i, ClientOrgMismatchError as it, checkServerReachable as j, checkServerConfig as k, reconcileLocalRuntimeProviders as l, cleanWorkspaces as lt, formatCheckReport as m, deriveHubUrlFromToken as n, setJsonMode as nt, promptUpdate as o, FirstTreeHubSDK as ot, promptMissingFields as p, formatStaleReason as q, registerSaaSConnectCommand as r, status as rt, startServer as s, SdkError as st, HubUrlDerivationError as t, print as tt, uploadClientCapabilities as u, probeCapabilities as ut, saveOnboardState as v, checkBackgroundService as w, migrateLocalAgentDirs as x, runHomeMigration as y, uninstallClientService as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-team-foundation/first-tree-hub",
3
- "version": "0.10.8",
3
+ "version": "0.10.9",
4
4
  "type": "module",
5
5
  "description": "First Tree Hub — unified CLI for server, client, and agent management",
6
6
  "exports": {