@agent-team-foundation/first-tree-hub 0.6.2 → 0.6.3
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-DW7aIpmE.mjs → bootstrap-DNL1cEwv.mjs} +10 -5
- package/dist/cli/index.mjs +98 -103
- package/dist/{core-RXUUKkCO.mjs → core-B10jgThe.mjs} +62 -26
- package/dist/drizzle/0020_unified_user_token.sql +50 -44
- package/dist/{feishu-BZ8pnMrQ.mjs → feishu-BoMJHlOv.mjs} +1 -1
- package/dist/index.mjs +3 -3
- package/dist/web/assets/{index-BMOr9-X2.js → index-CnLpaSBg.js} +2 -2
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
|
@@ -56,6 +56,10 @@ const clientConfigSchema = defineConfig({
|
|
|
56
56
|
default: "http://localhost:8000"
|
|
57
57
|
}
|
|
58
58
|
}) },
|
|
59
|
+
client: { id: field(z.string().regex(/^client_[a-f0-9]{8}$/), {
|
|
60
|
+
auto: "client-id",
|
|
61
|
+
env: "FIRST_TREE_HUB_CLIENT_ID"
|
|
62
|
+
}) },
|
|
59
63
|
logLevel: field(z.enum([
|
|
60
64
|
"debug",
|
|
61
65
|
"info",
|
|
@@ -112,6 +116,7 @@ function coerceEnvValue(value, schema) {
|
|
|
112
116
|
return value;
|
|
113
117
|
}
|
|
114
118
|
function builtinAutoGenerate(strategy) {
|
|
119
|
+
if (strategy === "client-id") return `client_${randomBytes(4).toString("hex")}`;
|
|
115
120
|
const match = /^random:(\w+):(\d+)$/.exec(strategy);
|
|
116
121
|
if (!match) throw new Error(`Unknown auto-generation strategy: ${strategy}`);
|
|
117
122
|
const encoding = match[1];
|
|
@@ -505,13 +510,13 @@ function resolveServerUrl(flagValue) {
|
|
|
505
510
|
* Resolve the current member access JWT from persisted credentials.
|
|
506
511
|
*
|
|
507
512
|
* Unified-user-token milestone: the CLI has a single credential store and a
|
|
508
|
-
* single onboarding path (`first-tree-hub connect`). The legacy
|
|
513
|
+
* single onboarding path (`first-tree-hub client connect`). The legacy
|
|
509
514
|
* `FIRST_TREE_HUB_TOKEN` env var is no longer read — callers get a clear
|
|
510
|
-
* error pointing at `connect` instead.
|
|
515
|
+
* error pointing at `client connect` instead.
|
|
511
516
|
*/
|
|
512
517
|
function resolveAccessToken() {
|
|
513
518
|
const creds = loadCredentials();
|
|
514
|
-
if (!creds) throw new Error("No credentials found. Run `first-tree-hub connect <server-url>` to sign in.");
|
|
519
|
+
if (!creds) throw new Error("No credentials found. Run `first-tree-hub client connect <server-url>` to sign in.");
|
|
515
520
|
return creds.accessToken;
|
|
516
521
|
}
|
|
517
522
|
/**
|
|
@@ -521,7 +526,7 @@ function resolveAccessToken() {
|
|
|
521
526
|
*/
|
|
522
527
|
async function ensureFreshAccessToken() {
|
|
523
528
|
const creds = loadCredentials();
|
|
524
|
-
if (!creds) throw new Error("No credentials found. Run `first-tree-hub connect <server-url>` to sign in.");
|
|
529
|
+
if (!creds) throw new Error("No credentials found. Run `first-tree-hub client connect <server-url>` to sign in.");
|
|
525
530
|
if (!isTokenExpired(creds.accessToken)) return creds.accessToken;
|
|
526
531
|
const res = await fetch(`${creds.serverUrl}/api/v1/auth/refresh`, {
|
|
527
532
|
method: "POST",
|
|
@@ -529,7 +534,7 @@ async function ensureFreshAccessToken() {
|
|
|
529
534
|
body: JSON.stringify({ refreshToken: creds.refreshToken }),
|
|
530
535
|
signal: AbortSignal.timeout(1e4)
|
|
531
536
|
});
|
|
532
|
-
if (!res.ok) throw new Error("Access token expired and refresh failed. Run `first-tree-hub connect <server-url>`.");
|
|
537
|
+
if (!res.ok) throw new Error("Access token expired and refresh failed. Run `first-tree-hub client connect <server-url>`.");
|
|
533
538
|
const data = await res.json();
|
|
534
539
|
saveCredentials({
|
|
535
540
|
...creds,
|
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 SdkError, D as createOwner, E as ClientRuntime, M as cleanWorkspaces, T as stopPostgres, _ as checkServerHealth, a as formatCheckReport, b as printResults, c as onboardCreate, d as checkAgentConfigs, f as checkClientConfig, g as checkServerConfig, h as checkNodeVersion, i as promptMissingFields, j as SessionRegistry, k as FirstTreeHubSDK, l as saveOnboardState, m as checkDocker, n as isInteractive, o as loadOnboardState, p as checkDatabase, r as promptAddAgent, s as onboardCheck, t as startServer, u as runMigrations, v as checkServerReachable, y as checkWebSocket } 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-DNL1cEwv.mjs";
|
|
3
|
+
import { A as SdkError, D as createOwner, E as ClientRuntime, M as cleanWorkspaces, T as stopPostgres, _ as checkServerHealth, a as formatCheckReport, b as printResults, c as onboardCreate, d as checkAgentConfigs, f as checkClientConfig, g as checkServerConfig, h as checkNodeVersion, i as promptMissingFields, j as SessionRegistry, k as FirstTreeHubSDK, l as saveOnboardState, m as checkDocker, n as isInteractive, o as loadOnboardState, p as checkDatabase, r as promptAddAgent, s as onboardCheck, t as startServer, u as runMigrations, v as checkServerReachable, y as checkWebSocket } from "../core-B10jgThe.mjs";
|
|
4
|
+
import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-BoMJHlOv.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";
|
|
@@ -362,7 +362,7 @@ function registerAgentCommands(program) {
|
|
|
362
362
|
process.stderr.write(" No agents configured.\n");
|
|
363
363
|
}
|
|
364
364
|
});
|
|
365
|
-
agent.command("create <name>").description("Create an agent on Hub and bind it locally").requiredOption("--type <type>", "Agent type (human, personal_assistant, autonomous_agent)").requiredOption("--client-id <id>", "Client (machine) that will run this agent — must be owned by you. Run `first-tree-hub connect` on that machine first.").option("--runtime <runtime>", "Runtime handler (default: claude-code)", "claude-code").option("--display-name <name>", "Display name").option("--server <url>", "Hub server URL").action(async (name, options) => {
|
|
365
|
+
agent.command("create <name>").description("Create an agent on Hub and bind it locally").requiredOption("--type <type>", "Agent type (human, personal_assistant, autonomous_agent)").requiredOption("--client-id <id>", "Client (machine) that will run this agent — must be owned by you. Run `first-tree-hub client connect` on that machine first.").option("--runtime <runtime>", "Runtime handler (default: claude-code)", "claude-code").option("--display-name <name>", "Display name").option("--server <url>", "Hub server URL").action(async (name, options) => {
|
|
366
366
|
try {
|
|
367
367
|
const serverUrl = resolveServerUrl(options.server);
|
|
368
368
|
const headers = {
|
|
@@ -740,9 +740,94 @@ function registerAgentCommands(program) {
|
|
|
740
740
|
});
|
|
741
741
|
}
|
|
742
742
|
//#endregion
|
|
743
|
+
//#region src/commands/connect.ts
|
|
744
|
+
/**
|
|
745
|
+
* Authenticate via connect token — exchange for full JWT credentials.
|
|
746
|
+
*/
|
|
747
|
+
async function authenticateWithToken(url, token) {
|
|
748
|
+
const res = await fetch(`${url}/api/v1/auth/connect-token`, {
|
|
749
|
+
method: "POST",
|
|
750
|
+
headers: { "Content-Type": "application/json" },
|
|
751
|
+
body: JSON.stringify({ token }),
|
|
752
|
+
signal: AbortSignal.timeout(1e4)
|
|
753
|
+
});
|
|
754
|
+
if (!res.ok) fail("AUTH_ERROR", (await res.json().catch(() => ({}))).error ?? `Token exchange failed (HTTP ${res.status})`, 1);
|
|
755
|
+
return await res.json();
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Authenticate via interactive username/password login.
|
|
759
|
+
*/
|
|
760
|
+
async function authenticateInteractive(url) {
|
|
761
|
+
process.stderr.write("\n Log in to Hub:\n");
|
|
762
|
+
const username = await input({ message: " Username:" });
|
|
763
|
+
const pw = await password({ message: " Password:" });
|
|
764
|
+
const loginRes = await fetch(`${url}/api/v1/auth/login`, {
|
|
765
|
+
method: "POST",
|
|
766
|
+
headers: { "Content-Type": "application/json" },
|
|
767
|
+
body: JSON.stringify({
|
|
768
|
+
username,
|
|
769
|
+
password: pw
|
|
770
|
+
}),
|
|
771
|
+
signal: AbortSignal.timeout(1e4)
|
|
772
|
+
});
|
|
773
|
+
if (!loginRes.ok) fail("AUTH_ERROR", (await loginRes.json().catch(() => ({}))).error ?? `Login failed (HTTP ${loginRes.status})`, 1);
|
|
774
|
+
return await loginRes.json();
|
|
775
|
+
}
|
|
776
|
+
function registerConnectCommand(parent) {
|
|
777
|
+
parent.command("connect <server-url>").description("Connect to a Hub server — configure, authenticate, and start client").option("--token <token>", "Connect token (from Hub web console) — skips interactive login").action(async (serverUrl, options) => {
|
|
778
|
+
try {
|
|
779
|
+
const url = serverUrl.replace(/\/+$/, "");
|
|
780
|
+
setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", url);
|
|
781
|
+
process.stderr.write(`\n \u2713 Server configured: ${url}\n`);
|
|
782
|
+
saveCredentials({
|
|
783
|
+
...options.token ? await authenticateWithToken(url, options.token) : await authenticateInteractive(url),
|
|
784
|
+
serverUrl: url
|
|
785
|
+
});
|
|
786
|
+
process.stderr.write(" ✓ Authenticated\n");
|
|
787
|
+
resetConfig();
|
|
788
|
+
resetConfigMeta();
|
|
789
|
+
const config = await initConfig({
|
|
790
|
+
schema: clientConfigSchema,
|
|
791
|
+
role: "client"
|
|
792
|
+
});
|
|
793
|
+
const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
|
|
794
|
+
const agents = loadAgents({
|
|
795
|
+
schema: agentConfigSchema,
|
|
796
|
+
agentsDir
|
|
797
|
+
});
|
|
798
|
+
process.stderr.write(`\n Starting client (id: ${config.client.id})...\n`);
|
|
799
|
+
const runtime = new ClientRuntime(config.server.url, config.client.id);
|
|
800
|
+
for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
|
|
801
|
+
await runtime.start();
|
|
802
|
+
runtime.watchAgentsDir(agentsDir);
|
|
803
|
+
const shutdown = async () => {
|
|
804
|
+
process.stderr.write("\n Shutting down...\n");
|
|
805
|
+
runtime.unwatchAgentsDir();
|
|
806
|
+
await runtime.stop();
|
|
807
|
+
process.exit(0);
|
|
808
|
+
};
|
|
809
|
+
process.on("SIGINT", () => void shutdown());
|
|
810
|
+
process.on("SIGTERM", () => void shutdown());
|
|
811
|
+
await new Promise(() => {});
|
|
812
|
+
} catch (error) {
|
|
813
|
+
if (error.name === "ExitPromptError") {
|
|
814
|
+
process.stderr.write("\n Cancelled.\n");
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
818
|
+
process.stderr.write(` Error: ${msg}\n`);
|
|
819
|
+
process.exit(1);
|
|
820
|
+
} finally {
|
|
821
|
+
resetConfig();
|
|
822
|
+
resetConfigMeta();
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
//#endregion
|
|
743
827
|
//#region src/commands/client.ts
|
|
744
828
|
function registerClientCommands(program) {
|
|
745
829
|
const client = program.command("client").description("Client runtime — connect agents to the server");
|
|
830
|
+
registerConnectCommand(client);
|
|
746
831
|
client.command("start").description("Start client — connect all configured agents to the server").option("--no-interactive", "Skip interactive prompts (for Docker/CI)").action(async (options) => {
|
|
747
832
|
try {
|
|
748
833
|
await promptMissingFields({
|
|
@@ -759,8 +844,8 @@ function registerClientCommands(program) {
|
|
|
759
844
|
schema: agentConfigSchema,
|
|
760
845
|
agentsDir
|
|
761
846
|
});
|
|
762
|
-
process.stderr.write(`\n Connecting to ${config.server.url}...\n`);
|
|
763
|
-
const runtime = new ClientRuntime(config.server.url);
|
|
847
|
+
process.stderr.write(`\n Connecting to ${config.server.url} (client id: ${config.client.id})...\n`);
|
|
848
|
+
const runtime = new ClientRuntime(config.server.url, config.client.id);
|
|
764
849
|
for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
|
|
765
850
|
await runtime.start();
|
|
766
851
|
runtime.watchAgentsDir(agentsDir);
|
|
@@ -961,101 +1046,17 @@ function isSecretField(schema, dotPath) {
|
|
|
961
1046
|
return false;
|
|
962
1047
|
}
|
|
963
1048
|
//#endregion
|
|
964
|
-
//#region src/commands/connect.ts
|
|
965
|
-
/**
|
|
966
|
-
* Authenticate via connect token — exchange for full JWT credentials.
|
|
967
|
-
*/
|
|
968
|
-
async function authenticateWithToken(url, token) {
|
|
969
|
-
const res = await fetch(`${url}/api/v1/auth/connect-token`, {
|
|
970
|
-
method: "POST",
|
|
971
|
-
headers: { "Content-Type": "application/json" },
|
|
972
|
-
body: JSON.stringify({ token }),
|
|
973
|
-
signal: AbortSignal.timeout(1e4)
|
|
974
|
-
});
|
|
975
|
-
if (!res.ok) fail("AUTH_ERROR", (await res.json().catch(() => ({}))).error ?? `Token exchange failed (HTTP ${res.status})`, 1);
|
|
976
|
-
return await res.json();
|
|
977
|
-
}
|
|
978
|
-
/**
|
|
979
|
-
* Authenticate via interactive username/password login.
|
|
980
|
-
*/
|
|
981
|
-
async function authenticateInteractive(url) {
|
|
982
|
-
process.stderr.write("\n Log in to Hub:\n");
|
|
983
|
-
const username = await input({ message: " Username:" });
|
|
984
|
-
const pw = await password({ message: " Password:" });
|
|
985
|
-
const loginRes = await fetch(`${url}/api/v1/auth/login`, {
|
|
986
|
-
method: "POST",
|
|
987
|
-
headers: { "Content-Type": "application/json" },
|
|
988
|
-
body: JSON.stringify({
|
|
989
|
-
username,
|
|
990
|
-
password: pw
|
|
991
|
-
}),
|
|
992
|
-
signal: AbortSignal.timeout(1e4)
|
|
993
|
-
});
|
|
994
|
-
if (!loginRes.ok) fail("AUTH_ERROR", (await loginRes.json().catch(() => ({}))).error ?? `Login failed (HTTP ${loginRes.status})`, 1);
|
|
995
|
-
return await loginRes.json();
|
|
996
|
-
}
|
|
997
|
-
function registerConnectCommand(program) {
|
|
998
|
-
program.command("connect <server-url>").description("Connect to a Hub server — configure, authenticate, and start client").option("--token <token>", "Connect token (from Hub web console) — skips interactive login").action(async (serverUrl, options) => {
|
|
999
|
-
try {
|
|
1000
|
-
const url = serverUrl.replace(/\/+$/, "");
|
|
1001
|
-
setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", url);
|
|
1002
|
-
process.stderr.write(`\n \u2713 Server configured: ${url}\n`);
|
|
1003
|
-
saveCredentials({
|
|
1004
|
-
...options.token ? await authenticateWithToken(url, options.token) : await authenticateInteractive(url),
|
|
1005
|
-
serverUrl: url
|
|
1006
|
-
});
|
|
1007
|
-
process.stderr.write(" ✓ Authenticated\n");
|
|
1008
|
-
resetConfig();
|
|
1009
|
-
resetConfigMeta();
|
|
1010
|
-
const config = await initConfig({
|
|
1011
|
-
schema: clientConfigSchema,
|
|
1012
|
-
role: "client"
|
|
1013
|
-
});
|
|
1014
|
-
const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
|
|
1015
|
-
const agents = loadAgents({
|
|
1016
|
-
schema: agentConfigSchema,
|
|
1017
|
-
agentsDir
|
|
1018
|
-
});
|
|
1019
|
-
process.stderr.write(`\n Starting client...\n`);
|
|
1020
|
-
const runtime = new ClientRuntime(config.server.url);
|
|
1021
|
-
for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
|
|
1022
|
-
await runtime.start();
|
|
1023
|
-
runtime.watchAgentsDir(agentsDir);
|
|
1024
|
-
const shutdown = async () => {
|
|
1025
|
-
process.stderr.write("\n Shutting down...\n");
|
|
1026
|
-
runtime.unwatchAgentsDir();
|
|
1027
|
-
await runtime.stop();
|
|
1028
|
-
process.exit(0);
|
|
1029
|
-
};
|
|
1030
|
-
process.on("SIGINT", () => void shutdown());
|
|
1031
|
-
process.on("SIGTERM", () => void shutdown());
|
|
1032
|
-
await new Promise(() => {});
|
|
1033
|
-
} catch (error) {
|
|
1034
|
-
if (error.name === "ExitPromptError") {
|
|
1035
|
-
process.stderr.write("\n Cancelled.\n");
|
|
1036
|
-
return;
|
|
1037
|
-
}
|
|
1038
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1039
|
-
process.stderr.write(` Error: ${msg}\n`);
|
|
1040
|
-
process.exit(1);
|
|
1041
|
-
} finally {
|
|
1042
|
-
resetConfig();
|
|
1043
|
-
resetConfigMeta();
|
|
1044
|
-
}
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
|
-
//#endregion
|
|
1048
1049
|
//#region src/commands/onboard.ts
|
|
1049
1050
|
async function promptMissing(args) {
|
|
1050
1051
|
if (!args.server) try {
|
|
1051
|
-
const { resolveServerUrl } = await import("../bootstrap-
|
|
1052
|
+
const { resolveServerUrl } = await import("../bootstrap-DNL1cEwv.mjs").then((n) => n.t);
|
|
1052
1053
|
resolveServerUrl();
|
|
1053
1054
|
} catch {
|
|
1054
1055
|
args.server = await input({ message: "Hub server URL:" });
|
|
1055
1056
|
saveOnboardState(args);
|
|
1056
1057
|
}
|
|
1057
|
-
const { loadCredentials } = await import("../bootstrap-
|
|
1058
|
-
if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub connect <server-url>` before onboarding.");
|
|
1058
|
+
const { loadCredentials } = await import("../bootstrap-DNL1cEwv.mjs").then((n) => n.t);
|
|
1059
|
+
if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub client connect <server-url>` before onboarding.");
|
|
1059
1060
|
if (!args.id) {
|
|
1060
1061
|
args.id = await input({ message: "Agent ID:" });
|
|
1061
1062
|
saveOnboardState(args);
|
|
@@ -1080,11 +1081,9 @@ async function promptMissing(args) {
|
|
|
1080
1081
|
});
|
|
1081
1082
|
saveOnboardState(args);
|
|
1082
1083
|
}
|
|
1083
|
-
if (args.type !== "human" &&
|
|
1084
|
-
args.clientId = await input({
|
|
1085
|
-
|
|
1086
|
-
validate: (v) => v.length > 0 ? true : "clientId is required for non-human agents"
|
|
1087
|
-
});
|
|
1084
|
+
if (args.type !== "human" && args.clientId === void 0) {
|
|
1085
|
+
args.clientId = await input({ message: "Client ID (Enter to leave unbound — first WS connect will claim it):" });
|
|
1086
|
+
if (!args.clientId) args.clientId = void 0;
|
|
1088
1087
|
saveOnboardState(args);
|
|
1089
1088
|
}
|
|
1090
1089
|
if (!args.role) {
|
|
@@ -1117,10 +1116,7 @@ async function promptMissing(args) {
|
|
|
1117
1116
|
message: "Assistant ID:",
|
|
1118
1117
|
default: `${args.id}-assistant`
|
|
1119
1118
|
});
|
|
1120
|
-
if (
|
|
1121
|
-
message: "Client ID for the assistant (must be owned by you):",
|
|
1122
|
-
validate: (v) => v.length > 0 ? true : "clientId is required"
|
|
1123
|
-
});
|
|
1119
|
+
if (args.clientId === void 0) args.clientId = await input({ message: "Client ID for the assistant (Enter to leave unbound):" }) || void 0;
|
|
1124
1120
|
saveOnboardState(args);
|
|
1125
1121
|
}
|
|
1126
1122
|
}
|
|
@@ -1327,7 +1323,6 @@ registerClientCommands(program);
|
|
|
1327
1323
|
registerAgentCommands(program);
|
|
1328
1324
|
registerConfigCommands(program);
|
|
1329
1325
|
registerStatusCommand(program);
|
|
1330
|
-
registerConnectCommand(program);
|
|
1331
1326
|
registerOnboardCommand(program);
|
|
1332
1327
|
program.parse();
|
|
1333
1328
|
//#endregion
|
|
@@ -1,5 +1,5 @@
|
|
|
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 updateOrganizationSchema, 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 taskListQuerySchema, K as sessionOutputMessageSchema, L as loginSchema, M as delegateFeishuUserSchema, N as dryRunAgentRuntimeConfigSchema, O as createChatSchema, P as inboxPollQuerySchema, Q as updateMemberSchema, R as messageSourceSchema$1, S as agentTypeSchema$1, T as createAdapterConfigSchema, U as selfServiceFeishuBotSchema, V as refreshTokenSchema, W as sendMessageSchema, X as updateAgentRuntimeConfigSchema, Y as updateAdapterConfigSchema, Z as updateAgentSchema, _ as addParticipantSchema, a as AGENT_SELECTOR_HEADER$1, b as agentBindRequestSchema, c as AGENT_TYPES, d as SYSTEM_CONFIG_DEFAULTS, et as updateSystemConfigSchema, f as TASK_CREATOR_TYPES, g as WS_AUTH_FRAME_TIMEOUT_MS, h as TASK_TERMINAL_STATUSES, i as AGENT_BIND_REJECT_REASONS, j as createTaskSchema, k as createMemberSchema, l as AGENT_VISIBILITY, m as TASK_STATUSES, nt as wsAuthFrameSchema, o as AGENT_SOURCES, p as TASK_HEALTH_SIGNALS, q as sessionStateMessageSchema, s as AGENT_STATUSES, tt as updateTaskStatusSchema, 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-
|
|
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-DNL1cEwv.mjs";
|
|
2
|
+
import { $ as updateOrganizationSchema, 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 taskListQuerySchema, K as sessionOutputMessageSchema, L as loginSchema, M as delegateFeishuUserSchema, N as dryRunAgentRuntimeConfigSchema, O as createChatSchema, P as inboxPollQuerySchema, Q as updateMemberSchema, R as messageSourceSchema$1, S as agentTypeSchema$1, T as createAdapterConfigSchema, U as selfServiceFeishuBotSchema, V as refreshTokenSchema, W as sendMessageSchema, X as updateAgentRuntimeConfigSchema, Y as updateAdapterConfigSchema, Z as updateAgentSchema, _ as addParticipantSchema, a as AGENT_SELECTOR_HEADER$1, b as agentBindRequestSchema, c as AGENT_TYPES, d as SYSTEM_CONFIG_DEFAULTS, et as updateSystemConfigSchema, f as TASK_CREATOR_TYPES, g as WS_AUTH_FRAME_TIMEOUT_MS, h as TASK_TERMINAL_STATUSES, i as AGENT_BIND_REJECT_REASONS, j as createTaskSchema, k as createMemberSchema, l as AGENT_VISIBILITY, m as TASK_STATUSES, nt as wsAuthFrameSchema, o as AGENT_SOURCES, p as TASK_HEALTH_SIGNALS, q as sessionStateMessageSchema, s as AGENT_STATUSES, tt as updateTaskStatusSchema, 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-BoMJHlOv.mjs";
|
|
3
3
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { ZodError, z } from "zod";
|
|
@@ -11,7 +11,7 @@ import WebSocket from "ws";
|
|
|
11
11
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
12
12
|
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
13
13
|
import bcrypt from "bcrypt";
|
|
14
|
-
import { and, asc, count, desc, eq, gt, inArray, isNotNull, lt, ne, or, sql } from "drizzle-orm";
|
|
14
|
+
import { and, asc, count, desc, eq, gt, inArray, isNotNull, isNull, lt, ne, or, sql } from "drizzle-orm";
|
|
15
15
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
16
16
|
import postgres from "postgres";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
@@ -172,7 +172,7 @@ z.object({
|
|
|
172
172
|
visibility: agentVisibilitySchema.optional(),
|
|
173
173
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
174
174
|
managerId: z.string().nullable().optional(),
|
|
175
|
-
clientId: z.string().optional()
|
|
175
|
+
clientId: z.string().min(1).max(100).nullable().optional()
|
|
176
176
|
});
|
|
177
177
|
z.object({
|
|
178
178
|
uuid: z.string(),
|
|
@@ -1277,6 +1277,10 @@ defineConfig({
|
|
|
1277
1277
|
default: "http://localhost:8000"
|
|
1278
1278
|
}
|
|
1279
1279
|
}) },
|
|
1280
|
+
client: { id: field(z.string().regex(/^client_[a-f0-9]{8}$/), {
|
|
1281
|
+
auto: "client-id",
|
|
1282
|
+
env: "FIRST_TREE_HUB_CLIENT_ID"
|
|
1283
|
+
}) },
|
|
1280
1284
|
logLevel: field(z.enum([
|
|
1281
1285
|
"debug",
|
|
1282
1286
|
"info",
|
|
@@ -3036,10 +3040,11 @@ var ClientRuntime = class {
|
|
|
3036
3040
|
agentNames = /* @__PURE__ */ new Set();
|
|
3037
3041
|
watcher = null;
|
|
3038
3042
|
debounceTimer = null;
|
|
3039
|
-
constructor(serverUrl) {
|
|
3043
|
+
constructor(serverUrl, clientId) {
|
|
3040
3044
|
this.serverUrl = serverUrl;
|
|
3041
3045
|
this.connection = new ClientConnection({
|
|
3042
3046
|
serverUrl,
|
|
3047
|
+
clientId,
|
|
3043
3048
|
getAccessToken: () => ensureFreshAccessToken()
|
|
3044
3049
|
});
|
|
3045
3050
|
registerBuiltinHandlers();
|
|
@@ -3655,7 +3660,7 @@ async function onboardCheck(args) {
|
|
|
3655
3660
|
key: "connect",
|
|
3656
3661
|
label: "Signed in",
|
|
3657
3662
|
status: "missing_required",
|
|
3658
|
-
hint: "Run `first-tree-hub connect <server-url>` first"
|
|
3663
|
+
hint: "Run `first-tree-hub client connect <server-url>` first"
|
|
3659
3664
|
});
|
|
3660
3665
|
try {
|
|
3661
3666
|
const serverUrl = resolveServerUrl(args.server);
|
|
@@ -3713,11 +3718,17 @@ async function onboardCheck(args) {
|
|
|
3713
3718
|
status: "missing_required",
|
|
3714
3719
|
hint: "Provide via --type"
|
|
3715
3720
|
});
|
|
3716
|
-
if (args.type && args.type !== "human"
|
|
3721
|
+
if (args.type && args.type !== "human") if (args.clientId) items.push({
|
|
3717
3722
|
key: "client",
|
|
3718
3723
|
label: "Target client",
|
|
3719
|
-
status: "
|
|
3720
|
-
|
|
3724
|
+
status: "ok",
|
|
3725
|
+
value: args.clientId
|
|
3726
|
+
});
|
|
3727
|
+
else items.push({
|
|
3728
|
+
key: "client",
|
|
3729
|
+
label: "Target client",
|
|
3730
|
+
status: "ok",
|
|
3731
|
+
value: "(unbound — claimed on first WS connect)"
|
|
3721
3732
|
});
|
|
3722
3733
|
return items;
|
|
3723
3734
|
}
|
|
@@ -3788,7 +3799,7 @@ async function onboardCreate(args) {
|
|
|
3788
3799
|
}
|
|
3789
3800
|
const runtimeAgent = args.type === "human" ? args.assistant : args.id;
|
|
3790
3801
|
if (args.feishuBotAppId && args.feishuBotAppSecret) {
|
|
3791
|
-
const { bindFeishuBot } = await import("./feishu-
|
|
3802
|
+
const { bindFeishuBot } = await import("./feishu-BoMJHlOv.mjs").then((n) => n.r);
|
|
3792
3803
|
const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
|
|
3793
3804
|
if (!targetAgentUuid) process.stderr.write(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
|
|
3794
3805
|
else {
|
|
@@ -3929,7 +3940,7 @@ function setNestedByDot(obj, dotPath, value) {
|
|
|
3929
3940
|
if (lastKey !== void 0) current[lastKey] = value;
|
|
3930
3941
|
}
|
|
3931
3942
|
//#endregion
|
|
3932
|
-
//#region ../server/dist/app-
|
|
3943
|
+
//#region ../server/dist/app-BjIHPMKY.mjs
|
|
3933
3944
|
var __defProp = Object.defineProperty;
|
|
3934
3945
|
var __exportAll = (all, no_symbols) => {
|
|
3935
3946
|
let target = {};
|
|
@@ -4852,18 +4863,20 @@ function defaultVisibility(type) {
|
|
|
4852
4863
|
/**
|
|
4853
4864
|
* Resolve + validate the client that will own the new agent.
|
|
4854
4865
|
*
|
|
4855
|
-
* Rule (unified-user-token
|
|
4856
|
-
* -
|
|
4857
|
-
*
|
|
4858
|
-
* -
|
|
4859
|
-
*
|
|
4866
|
+
* Rule (unified-user-token, post-first-bind relaxation):
|
|
4867
|
+
* - Human agents represent the member themselves and have no runtime; a
|
|
4868
|
+
* missing `clientId` is required and the column stays NULL.
|
|
4869
|
+
* - Non-human agents MAY omit `clientId` at creation; the row stays NULL
|
|
4870
|
+
* and is claimed on the first WS bind (see `api/agent/ws-client.ts`).
|
|
4871
|
+
* - When a non-human agent IS created with a `clientId`, the pinned client
|
|
4872
|
+
* must already be owned by the manager's user (Rule R-RUN).
|
|
4860
4873
|
*/
|
|
4861
4874
|
async function resolveAgentClient(db, data) {
|
|
4862
4875
|
if (data.type === "human") {
|
|
4863
4876
|
if (data.clientId) throw new BadRequestError("Human agents cannot be pinned to a client");
|
|
4864
4877
|
return null;
|
|
4865
4878
|
}
|
|
4866
|
-
if (!data.clientId)
|
|
4879
|
+
if (!data.clientId) return null;
|
|
4867
4880
|
const [manager] = await db.select({ userId: members.userId }).from(members).where(eq(members.id, data.managerId)).limit(1);
|
|
4868
4881
|
if (!manager) throw new BadRequestError(`Manager "${data.managerId}" not found`);
|
|
4869
4882
|
const [client] = await db.select({
|
|
@@ -4871,7 +4884,7 @@ async function resolveAgentClient(db, data) {
|
|
|
4871
4884
|
userId: clients.userId
|
|
4872
4885
|
}).from(clients).where(eq(clients.id, data.clientId)).limit(1);
|
|
4873
4886
|
if (!client) throw new BadRequestError(`Client "${data.clientId}" not found`);
|
|
4874
|
-
if (!client.userId) throw new BadRequestError(`Client "${data.clientId}" has not been claimed by a user yet. Have the operator run \`first-tree-hub connect\` on that machine before pinning an agent to it.`);
|
|
4887
|
+
if (!client.userId) throw new BadRequestError(`Client "${data.clientId}" has not been claimed by a user yet. Have the operator run \`first-tree-hub client connect\` on that machine before pinning an agent to it.`);
|
|
4875
4888
|
if (client.userId !== manager.userId) throw new ForbiddenError(`Client "${data.clientId}" is not owned by the manager's user — pick a client belonging to that user.`);
|
|
4876
4889
|
return client.id;
|
|
4877
4890
|
}
|
|
@@ -4973,8 +4986,11 @@ async function listAgentsForMember(db, scope, limit, cursor, type) {
|
|
|
4973
4986
|
};
|
|
4974
4987
|
}
|
|
4975
4988
|
async function updateAgent(db, uuid, data) {
|
|
4976
|
-
if (data.clientId !== void 0) throw new BadRequestError("clientId is immutable in this milestone — delete and re-create the agent on the target client to move it");
|
|
4977
4989
|
const agent = await getAgent(db, uuid);
|
|
4990
|
+
if (data.clientId !== void 0) {
|
|
4991
|
+
if (data.clientId === null) throw new BadRequestError("clientId cannot be cleared — once bound, an agent stays bound to its client");
|
|
4992
|
+
if (agent.clientId !== null && agent.clientId !== data.clientId) throw new BadRequestError("clientId is immutable once set — delete and re-create the agent on the target client to move it");
|
|
4993
|
+
}
|
|
4978
4994
|
const updates = { updatedAt: /* @__PURE__ */ new Date() };
|
|
4979
4995
|
if (data.type !== void 0) updates.type = data.type;
|
|
4980
4996
|
if (data.displayName !== void 0) updates.displayName = data.displayName;
|
|
@@ -4991,6 +5007,14 @@ async function updateAgent(db, uuid, data) {
|
|
|
4991
5007
|
if (manager.organizationId !== agent.organizationId) throw new BadRequestError("Manager must belong to the same organization as the agent");
|
|
4992
5008
|
updates.managerId = data.managerId;
|
|
4993
5009
|
}
|
|
5010
|
+
if (data.clientId !== void 0 && data.clientId !== null && agent.clientId === null) {
|
|
5011
|
+
const resolvedClientId = await resolveAgentClient(db, {
|
|
5012
|
+
clientId: data.clientId,
|
|
5013
|
+
managerId: updates.managerId ?? agent.managerId,
|
|
5014
|
+
type: agent.type
|
|
5015
|
+
});
|
|
5016
|
+
if (resolvedClientId !== null) updates.clientId = resolvedClientId;
|
|
5017
|
+
}
|
|
4994
5018
|
const [updated] = await db.update(agents).set(updates).where(eq(agents.uuid, agent.uuid)).returning();
|
|
4995
5019
|
if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
|
|
4996
5020
|
return updated;
|
|
@@ -5982,7 +6006,7 @@ async function adminAgentRoutes(app) {
|
|
|
5982
6006
|
};
|
|
5983
6007
|
if (health === "disconnected") return reply.status(200).send({
|
|
5984
6008
|
status: "offline",
|
|
5985
|
-
message: "Agent is not connected. Start the client with: first-tree-hub connect <server-url>",
|
|
6009
|
+
message: "Agent is not connected. Start the client with: first-tree-hub client connect <server-url>",
|
|
5986
6010
|
connection
|
|
5987
6011
|
});
|
|
5988
6012
|
if (health === "stale") return reply.status(200).send({
|
|
@@ -8226,8 +8250,9 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
8226
8250
|
inboxId: agents.inboxId,
|
|
8227
8251
|
status: agents.status,
|
|
8228
8252
|
clientId: agents.clientId,
|
|
8229
|
-
clientUserId: clients.userId
|
|
8230
|
-
|
|
8253
|
+
clientUserId: clients.userId,
|
|
8254
|
+
managerUserId: members.userId
|
|
8255
|
+
}).from(agents).leftJoin(clients, eq(agents.clientId, clients.id)).leftJoin(members, eq(agents.managerId, members.id)).where(and(eq(agents.uuid, bindRequest.agentId))).limit(1);
|
|
8231
8256
|
if (!agent) {
|
|
8232
8257
|
sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.UNKNOWN_AGENT);
|
|
8233
8258
|
return;
|
|
@@ -8240,11 +8265,22 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
8240
8265
|
sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.AGENT_SUSPENDED);
|
|
8241
8266
|
return;
|
|
8242
8267
|
}
|
|
8243
|
-
if (
|
|
8268
|
+
if (agent.clientId === null) {
|
|
8269
|
+
if (!agent.managerUserId || agent.managerUserId !== session.userId) {
|
|
8270
|
+
sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.NOT_OWNED);
|
|
8271
|
+
return;
|
|
8272
|
+
}
|
|
8273
|
+
if ((await app.db.update(agents).set({
|
|
8274
|
+
clientId,
|
|
8275
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
8276
|
+
}).where(and(eq(agents.uuid, agent.id), isNull(agents.clientId))).returning({ uuid: agents.uuid })).length === 0) {
|
|
8277
|
+
sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.WRONG_CLIENT);
|
|
8278
|
+
return;
|
|
8279
|
+
}
|
|
8280
|
+
} else if (agent.clientId !== clientId) {
|
|
8244
8281
|
sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.WRONG_CLIENT);
|
|
8245
8282
|
return;
|
|
8246
|
-
}
|
|
8247
|
-
if (!agent.clientUserId || agent.clientUserId !== session.userId) {
|
|
8283
|
+
} else if (!agent.clientUserId || agent.clientUserId !== session.userId) {
|
|
8248
8284
|
sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.NOT_OWNED);
|
|
8249
8285
|
return;
|
|
8250
8286
|
}
|
|
@@ -8601,7 +8637,7 @@ async function meRoutes(app) {
|
|
|
8601
8637
|
return {
|
|
8602
8638
|
token,
|
|
8603
8639
|
expiresIn,
|
|
8604
|
-
command: `first-tree-hub connect ${`${request.headers["x-forwarded-proto"] ?? request.protocol}://${request.headers["x-forwarded-host"] ?? request.headers.host ?? request.hostname}`} --token ${token}`
|
|
8640
|
+
command: `first-tree-hub client connect ${`${request.headers["x-forwarded-proto"] ?? request.protocol}://${request.headers["x-forwarded-host"] ?? request.headers.host ?? request.hostname}`} --token ${token}`
|
|
8605
8641
|
};
|
|
8606
8642
|
});
|
|
8607
8643
|
}
|