@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.
package/dist/cli/index.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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 };
|