@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.
- package/dist/{bootstrap-CRDR6NwE.mjs → bootstrap-8nCntTrK.mjs} +29 -13
- package/dist/cli/index.mjs +5 -5
- package/dist/{core-6-paFwyo.mjs → core-BA5U1v9L.mjs} +251 -5
- package/dist/{feishu-CJ08ntOD.mjs → feishu-D9JkMZnU.mjs} +14 -1
- package/dist/index.mjs +3 -3
- package/dist/web/assets/index-7iSpxOWW.js +333 -0
- package/dist/web/assets/index-Cze8BC63.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-30C-bada.js +0 -333
- package/dist/web/assets/index-COlVuDVR.css +0 -1
|
@@ -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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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;
|
package/dist/cli/index.mjs
CHANGED
|
@@ -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-
|
|
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-
|
|
4
|
-
import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-
|
|
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-
|
|
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-
|
|
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-
|
|
2
|
-
import { $ as
|
|
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-
|
|
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-
|
|
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 {
|
|
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-
|
|
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-
|
|
3
|
-
import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-
|
|
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 };
|