@agent-team-foundation/first-tree-hub 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,11 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-twds-ZHy.mjs";
2
- import { z } from "zod";
3
2
  import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
4
3
  import { dirname, join } from "node:path";
5
- import { parse, stringify } from "yaml";
6
4
  import { randomBytes } from "node:crypto";
7
- import { homedir } from "node:os";
8
5
  import { execSync } from "node:child_process";
6
+ import { z } from "zod";
7
+ import { parse, stringify } from "yaml";
8
+ import { homedir } from "node:os";
9
9
  //#region ../shared/dist/config/index.mjs
10
10
  /** Declare a config field with a Zod schema and optional metadata. */
11
11
  function field(schema, options) {
@@ -544,7 +544,7 @@ async function bootstrapToken(serverUrl, agentId, options = {}) {
544
544
  }
545
545
  const data = await res.json();
546
546
  if (options.saveTo === "agent" || !options.saveTo) {
547
- const configDir = join(homedir(), ".first-tree-hub", "agents", agentId);
547
+ const configDir = join(DEFAULT_CONFIG_DIR, "agents", agentId);
548
548
  const configPath = `${configDir}/agent.yaml`;
549
549
  mkdirSync(configDir, {
550
550
  recursive: true,
@@ -1,30 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { _ as resetConfig, b as serverConfigSchema, c as DEFAULT_CONFIG_DIR, d as clientConfigSchema, g as readConfigFile, h as loadAgents, l as DEFAULT_DATA_DIR, m as initConfig, o as resolveAgentToken, p as getConfigValue, s as resolveServerUrl, t as bootstrapToken, u as agentConfigSchema, v as resetConfigMeta, x as setConfigValue } from "../bootstrap-waEJYq3g.mjs";
3
- import { A as ClientRuntime, B as registerBuiltinHandlers, C as checkWebSocket, F as SdkError, I as SessionRegistry, L as cleanWorkspaces, M as AgentSlot, N as DEFAULT_WORKSPACE_TTL_MS, P as FirstTreeHubSDK, R as getHandlerFactory, S as checkServerReachable, V as createAdminUser, _ as checkDocker, a as formatCheckReport, b as checkServerConfig, c as onboardContinue, d as runMigrations, f as checkAgentConfigs, g as checkDatabase, h as checkContextTreeRepo, i as promptMissingFields, j as AgentRuntime, k as stopPostgres, l as onboardCreate, m as checkClientConfig, n as isInteractive, o as loadOnboardState, p as checkAgentTokens, r as promptAddAgent, s as onboardCheck, t as startServer, u as saveOnboardState, v as checkGitHubToken, w as printResults, x as checkServerHealth, y as checkNodeVersion, z as loadRuntimeConfig } from "../core-Bl6djdPd.mjs";
2
+ import { A as ClientRuntime, C as checkWebSocket, F as SessionRegistry, I as cleanWorkspaces, N as FirstTreeHubSDK, P as SdkError, S as checkServerReachable, _ as checkDocker, a as formatCheckReport, b as checkServerConfig, c as onboardContinue, d as runMigrations, f as checkAgentConfigs, g as checkDatabase, h as checkContextTreeRepo, i as promptMissingFields, j as createAdminUser, k as stopPostgres, l as onboardCreate, m as checkClientConfig, n as isInteractive, o as loadOnboardState, p as checkAgentTokens, r as promptAddAgent, s as onboardCheck, t as startServer, u as saveOnboardState, v as checkGitHubToken, w as printResults, x as checkServerHealth, y as checkNodeVersion } from "../core-CZjUVAU-.mjs";
3
+ import { _ as resetConfig, b as serverConfigSchema, c as DEFAULT_CONFIG_DIR, d as clientConfigSchema, g as readConfigFile, h as loadAgents, l as DEFAULT_DATA_DIR, m as initConfig, o as resolveAgentToken, p as getConfigValue, s as resolveServerUrl, t as bootstrapToken, u as agentConfigSchema, v as resetConfigMeta, x as setConfigValue } from "../bootstrap-CPdLNPme.mjs";
4
4
  import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-Y4m2zFc3.mjs";
5
5
  import { createRequire } from "node:module";
6
6
  import { Command } from "commander";
7
7
  import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import { confirm, input, select } from "@inquirer/prompts";
10
- //#region src/commands/admin.ts
11
- function registerAdminCommands(program) {
12
- program.command("admin").description("Admin user management").command("create").description("Create an admin user").option("-u, --username <name>", "Admin username", "admin").option("-p, --password <pass>", "Admin password (auto-generated if omitted)").action(async (options) => {
13
- try {
14
- const result = await createAdminUser((await initConfig({
15
- schema: serverConfigSchema,
16
- role: "server"
17
- })).database.url, options.username, options.password);
18
- process.stderr.write(` Admin user "${result.username}" created.\n`);
19
- if (!options.password) process.stderr.write(` Password: ${result.password} (save this — shown only once)\n`);
20
- } catch (error) {
21
- const msg = error instanceof Error ? error.message : String(error);
22
- process.stderr.write(` Error: ${msg}\n`);
23
- process.exit(1);
24
- }
25
- });
26
- }
27
- //#endregion
28
10
  //#region src/cli/output.ts
29
11
  /** Write a success JSON envelope to stdout. */
30
12
  function success(data) {
@@ -46,18 +28,25 @@ function fail(code, message, exitCode = 1) {
46
28
  }
47
29
  //#endregion
48
30
  //#region src/commands/agent.ts
31
+ const DEFAULT_WORKSPACE_TTL_MS = 10080 * 60 * 1e3;
49
32
  function resolveAgentConfig() {
50
33
  const token = process.env.FIRST_TREE_HUB_TOKEN;
51
34
  if (!token) fail("MISSING_TOKEN", "FIRST_TREE_HUB_TOKEN environment variable is required.", 2);
35
+ let serverUrl;
36
+ try {
37
+ serverUrl = resolveServerUrl(process.env.FIRST_TREE_HUB_SERVER);
38
+ } catch {
39
+ serverUrl = "http://localhost:8000";
40
+ }
52
41
  return {
53
- serverUrl: process.env.FIRST_TREE_HUB_SERVER ?? "http://localhost:8000",
42
+ serverUrl,
54
43
  token
55
44
  };
56
45
  }
57
- function createSdk$1() {
46
+ function createSdk() {
58
47
  return new FirstTreeHubSDK(resolveAgentConfig());
59
48
  }
60
- function handleError$1(error) {
49
+ function handleSdkError(error) {
61
50
  if (error instanceof SdkError) {
62
51
  const exitCode = error.statusCode === 401 ? 3 : 1;
63
52
  fail(`HTTP_${error.statusCode}`, error.message, exitCode);
@@ -65,31 +54,132 @@ function handleError$1(error) {
65
54
  if (error instanceof TypeError && "cause" in error) fail("CONNECTION_ERROR", `Cannot connect to server: ${error.message}`, 6);
66
55
  fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), 1);
67
56
  }
57
+ function parseLimit(value, max) {
58
+ const limit = Number.parseInt(value, 10);
59
+ if (Number.isNaN(limit) || limit < 1 || limit > max) fail("INVALID_LIMIT", `Limit must be between 1 and ${max}.`, 2);
60
+ return limit;
61
+ }
62
+ const MAX_STDIN_BYTES = 10 * 1024 * 1024;
63
+ function readStdin() {
64
+ if (process.stdin.isTTY) return Promise.resolve(null);
65
+ return new Promise((resolve, reject) => {
66
+ const chunks = [];
67
+ let totalSize = 0;
68
+ process.stdin.on("data", (chunk) => {
69
+ totalSize += chunk.length;
70
+ if (totalSize > MAX_STDIN_BYTES) {
71
+ process.stdin.destroy();
72
+ reject(/* @__PURE__ */ new Error(`stdin exceeds ${MAX_STDIN_BYTES} bytes`));
73
+ return;
74
+ }
75
+ chunks.push(chunk);
76
+ });
77
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
78
+ process.stdin.on("error", reject);
79
+ });
80
+ }
68
81
  function registerAgentCommands(program) {
69
- program.command("register").description("Register this agent and return identity info").action(async () => {
82
+ const agent = program.command("agent").description("Agent management config, tokens, bindings, messaging");
83
+ agent.command("add [name]").description("Add an agent instance").option("-t, --token <token>", "Agent token").action(async (name, options) => {
70
84
  try {
71
- success(await createSdk$1().register());
85
+ let agentName = name;
86
+ let agentToken = options?.token;
87
+ if (!agentName || !agentToken) {
88
+ const result = await promptAddAgent();
89
+ agentName = agentName ?? result.name;
90
+ agentToken = agentToken ?? result.token;
91
+ }
92
+ const agentDir = join(DEFAULT_CONFIG_DIR, "agents", agentName);
93
+ mkdirSync(agentDir, {
94
+ recursive: true,
95
+ mode: 448
96
+ });
97
+ setConfigValue(join(agentDir, "agent.yaml"), "token", agentToken);
98
+ process.stderr.write(` Agent "${agentName}" added.\n`);
99
+ process.stderr.write(` Config: ${join(agentDir, "agent.yaml")}\n`);
72
100
  } catch (error) {
73
- handleError$1(error);
101
+ if (error.name === "ExitPromptError") {
102
+ process.stderr.write("\n Cancelled.\n");
103
+ return;
104
+ }
105
+ const msg = error instanceof Error ? error.message : String(error);
106
+ process.stderr.write(` Error: ${msg}\n`);
107
+ process.exit(1);
74
108
  }
75
109
  });
76
- program.command("pull").description("Pull pending messages from inbox").option("-l, --limit <number>", "Maximum entries to return", "10").option("-a, --ack", "Automatically ACK entries after pulling").action(async (options) => {
110
+ agent.command("remove <name>").description("Remove an agent instance and its runtime data").action((name) => {
111
+ const agentDir = join(DEFAULT_CONFIG_DIR, "agents", name);
112
+ if (!existsSync(agentDir)) {
113
+ process.stderr.write(` Agent "${name}" not found.\n`);
114
+ process.exit(1);
115
+ }
116
+ rmSync(agentDir, {
117
+ recursive: true,
118
+ force: true
119
+ });
120
+ rmSync(join(DEFAULT_DATA_DIR, "workspaces", name), {
121
+ recursive: true,
122
+ force: true
123
+ });
124
+ rmSync(join(DEFAULT_DATA_DIR, "sessions", `${name}.json`), { force: true });
125
+ process.stderr.write(` Agent "${name}" removed.\n`);
126
+ });
127
+ agent.command("list").description("List configured agents").action(() => {
128
+ const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
77
129
  try {
78
- const sdk = createSdk$1();
79
- const limit = Number.parseInt(options.limit, 10);
80
- if (Number.isNaN(limit) || limit < 1 || limit > 50) fail("INVALID_LIMIT", "Limit must be between 1 and 50.", 2);
81
- const result = await sdk.pull(limit);
82
- if (options.ack && result.entries.length > 0) await Promise.all(result.entries.map((entry) => sdk.ack(entry.id)));
83
- success(result);
130
+ const agents = loadAgents({
131
+ schema: agentConfigSchema,
132
+ agentsDir
133
+ });
134
+ if (agents.size === 0) {
135
+ process.stderr.write(" No agents configured.\n");
136
+ return;
137
+ }
138
+ for (const [name, config] of agents) {
139
+ const masked = config.token.length > 8 ? `${config.token.slice(0, 6)}***${config.token.slice(-2)}` : "***";
140
+ process.stderr.write(` ${name.padEnd(20)} type: ${config.type.padEnd(14)} token: ${masked}\n`);
141
+ }
142
+ } catch {
143
+ process.stderr.write(" No agents configured.\n");
144
+ }
145
+ });
146
+ agent.command("workspace").description("Manage agent workspaces").command("clean [agent-name]").description("Remove stale workspace directories (older than TTL with no active session)").option("--ttl <days>", "TTL in days", String(DEFAULT_WORKSPACE_TTL_MS / (1440 * 60 * 1e3))).action((agentName, options) => {
147
+ const defaultDays = DEFAULT_WORKSPACE_TTL_MS / (1440 * 60 * 1e3);
148
+ const ttlMs = Number.parseInt(options?.ttl ?? String(defaultDays), 10) * 24 * 60 * 60 * 1e3;
149
+ const workspacesDir = join(DEFAULT_DATA_DIR, "workspaces");
150
+ if (!existsSync(workspacesDir)) {
151
+ process.stderr.write(" No workspaces found.\n");
152
+ return;
153
+ }
154
+ const agentNames = agentName ? [agentName] : readdirSync(workspacesDir);
155
+ let totalRemoved = 0;
156
+ for (const name of agentNames) {
157
+ const agentWorkspaceRoot = join(workspacesDir, name);
158
+ if (!existsSync(agentWorkspaceRoot)) continue;
159
+ const persisted = new SessionRegistry(join(DEFAULT_DATA_DIR, "sessions", `${name}.json`)).load();
160
+ const activeChatIds = /* @__PURE__ */ new Set();
161
+ for (const [chatId, data] of persisted) if (data.status !== "evicted") activeChatIds.add(chatId);
162
+ const removed = cleanWorkspaces(agentWorkspaceRoot, activeChatIds, ttlMs);
163
+ totalRemoved += removed.length;
164
+ for (const chatId of removed) process.stderr.write(` Removed: ${name}/${chatId}\n`);
165
+ }
166
+ process.stderr.write(` ${totalRemoved} workspace(s) cleaned.\n`);
167
+ });
168
+ agent.command("token").description("Agent token management").command("bootstrap <agentId>").description("Bootstrap a token using GitHub identity (requires gh CLI)").option("--save-to <target>", "Save token to: \"agent\" (default) or a file path", "agent").option("--server <url>", "Hub server URL").action(async (agentId, options) => {
169
+ try {
170
+ const result = await bootstrapToken(resolveServerUrl(options.server), agentId, { saveTo: options.saveTo });
171
+ if (options.saveTo === "agent") process.stderr.write(`Token saved to ~/.first-tree-hub/config/agents/${agentId}/agent.yaml\n`);
172
+ else process.stderr.write(`Token saved to ${options.saveTo}\n`);
173
+ success({
174
+ agentId: result.agentId,
175
+ tokenSaved: true
176
+ });
84
177
  } catch (error) {
85
- handleError$1(error);
178
+ fail("BOOTSTRAP_ERROR", error instanceof Error ? error.message : String(error));
86
179
  }
87
180
  });
88
- }
89
- //#endregion
90
- //#region src/commands/bind.ts
91
- function registerBindCommands(program) {
92
- program.command("bind-bot").description("Bind a Feishu bot to this agent (self-service)").requiredOption("--platform <platform>", "Platform: feishu").requiredOption("--app-id <id>", "Feishu bot App ID").requiredOption("--app-secret <secret>", "Feishu bot App Secret").option("--server <url>", "Hub server URL").action(async (options) => {
181
+ const bind = agent.command("bind").description("Bind external IM accounts to agents");
182
+ bind.command("bot").description("Bind a Feishu bot to this agent (self-service)").requiredOption("--platform <platform>", "Platform: feishu").requiredOption("--app-id <id>", "Feishu bot App ID").requiredOption("--app-secret <secret>", "Feishu bot App Secret").option("--server <url>", "Hub server URL").action(async (options) => {
93
183
  try {
94
184
  if (options.platform !== "feishu") fail("UNSUPPORTED_PLATFORM", `Platform "${options.platform}" is not supported. Use "feishu".`);
95
185
  await bindFeishuBot(resolveServerUrl(options.server), resolveAgentToken(), options.appId, options.appSecret);
@@ -102,7 +192,7 @@ function registerBindCommands(program) {
102
192
  fail("BIND_BOT_ERROR", error instanceof Error ? error.message : String(error));
103
193
  }
104
194
  });
105
- program.command("bind-user <humanAgentId>").description("Bind a Feishu user to a human agent (via delegate_mention)").requiredOption("--platform <platform>", "Platform: feishu").requiredOption("--feishu-id <id>", "Feishu user ID (ou_xxx)").option("--server <url>", "Hub server URL").action(async (humanAgentId, options) => {
195
+ bind.command("user <humanAgentId>").description("Bind a Feishu user to a human agent (via delegate_mention)").requiredOption("--platform <platform>", "Platform: feishu").requiredOption("--feishu-id <id>", "Feishu user ID (ou_xxx)").option("--server <url>", "Hub server URL").action(async (humanAgentId, options) => {
106
196
  try {
107
197
  if (options.platform !== "feishu") fail("UNSUPPORTED_PLATFORM", `Platform "${options.platform}" is not supported. Use "feishu".`);
108
198
  await bindFeishuUser(resolveServerUrl(options.server), resolveAgentToken(), humanAgentId, options.feishuId);
@@ -116,42 +206,37 @@ function registerBindCommands(program) {
116
206
  fail("BIND_USER_ERROR", error instanceof Error ? error.message : String(error));
117
207
  }
118
208
  });
119
- }
120
- //#endregion
121
- //#region src/cli/util.ts
122
- function resolveConfig() {
123
- const token = process.env.FIRST_TREE_HUB_TOKEN;
124
- if (!token) fail("MISSING_TOKEN", "FIRST_TREE_HUB_TOKEN environment variable is required.", 2);
125
- return {
126
- serverUrl: process.env.FIRST_TREE_HUB_SERVER ?? "http://localhost:8000",
127
- token
128
- };
129
- }
130
- function createSdk() {
131
- return new FirstTreeHubSDK(resolveConfig());
132
- }
133
- function handleError(error) {
134
- if (error instanceof SdkError) {
135
- const exitCode = error.statusCode === 401 ? 3 : 1;
136
- fail(`HTTP_${error.statusCode}`, error.message, exitCode);
137
- }
138
- if (error instanceof TypeError && "cause" in error) fail("CONNECTION_ERROR", `Cannot connect to server: ${error.message}`, 6);
139
- fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), 1);
140
- }
141
- /** Parse and validate a numeric limit option from Commander string. */
142
- function parseLimit(value, max) {
143
- const limit = Number.parseInt(value, 10);
144
- if (Number.isNaN(limit) || limit < 1 || limit > max) fail("INVALID_LIMIT", `Limit must be between 1 and ${max}.`, 2);
145
- return limit;
146
- }
147
- /** Write a log line to stderr. */
148
- function log(tag, message) {
149
- process.stderr.write(`[${tag}] ${message}\n`);
150
- }
151
- //#endregion
152
- //#region src/commands/chats.ts
153
- function registerChatsCommand(program) {
154
- program.command("chats").description("List chats this agent participates in").option("-l, --limit <number>", "Maximum chats to return (1-100)", "20").option("--cursor <cursor>", "Pagination cursor from previous response").action(async (options) => {
209
+ agent.command("send <target> [message]").description("Send a message to an agent or chat").option("-f, --format <format>", "Message format (text|markdown|card)", "text").option("--chat", "Treat target as chat ID instead of agent ID").option("-m, --metadata <json>", "JSON metadata to attach").option("--reply-to <messageId>", "Message ID to reply to").option("--reply-to-inbox <inboxId>", "Cross-chat reply target inbox").option("--reply-to-chat <chatId>", "Cross-chat reply target chat").action(async (target, message, options) => {
210
+ try {
211
+ const content = message ?? await readStdin();
212
+ if (!content) fail("NO_MESSAGE", "No message provided. Pass as argument or pipe via stdin.", 2);
213
+ let metadata;
214
+ if (options.metadata) try {
215
+ metadata = JSON.parse(options.metadata);
216
+ } catch {
217
+ fail("INVALID_METADATA", "Metadata must be valid JSON.", 2);
218
+ }
219
+ const sdk = createSdk();
220
+ if (options.chat) success(await sdk.sendMessage(target, {
221
+ format: options.format,
222
+ content,
223
+ metadata,
224
+ inReplyTo: options.replyTo,
225
+ replyToInbox: options.replyToInbox,
226
+ replyToChat: options.replyToChat
227
+ }));
228
+ else success(await sdk.sendToAgent(target, {
229
+ format: options.format,
230
+ content,
231
+ metadata,
232
+ replyToInbox: options.replyToInbox,
233
+ replyToChat: options.replyToChat
234
+ }));
235
+ } catch (error) {
236
+ handleSdkError(error);
237
+ }
238
+ });
239
+ agent.command("chats").description("List chats this agent participates in").option("-l, --limit <number>", "Maximum chats to return (1-100)", "20").option("--cursor <cursor>", "Pagination cursor from previous response").action(async (options) => {
155
240
  try {
156
241
  const limit = parseLimit(options.limit, 100);
157
242
  success(await createSdk().listChats({
@@ -159,14 +244,43 @@ function registerChatsCommand(program) {
159
244
  cursor: options.cursor
160
245
  }));
161
246
  } catch (error) {
162
- handleError(error);
247
+ handleSdkError(error);
248
+ }
249
+ });
250
+ agent.command("history <chatId>").description("View message history in a chat").option("-l, --limit <number>", "Maximum messages to return (1-100)", "20").option("--cursor <cursor>", "Pagination cursor from previous response").action(async (chatId, options) => {
251
+ try {
252
+ const limit = parseLimit(options.limit, 100);
253
+ success(await createSdk().listMessages(chatId, {
254
+ limit,
255
+ cursor: options.cursor
256
+ }));
257
+ } catch (error) {
258
+ handleSdkError(error);
259
+ }
260
+ });
261
+ agent.command("register").description("Register this agent and return identity info").action(async () => {
262
+ try {
263
+ success(await createSdk().register());
264
+ } catch (error) {
265
+ handleSdkError(error);
266
+ }
267
+ });
268
+ agent.command("pull").description("Pull pending messages from inbox").option("-l, --limit <number>", "Maximum entries to return", "10").option("-a, --ack", "Automatically ACK entries after pulling").action(async (options) => {
269
+ try {
270
+ const sdk = createSdk();
271
+ const limit = parseLimit(options.limit, 50);
272
+ const result = await sdk.pull(limit);
273
+ if (options.ack && result.entries.length > 0) await Promise.all(result.entries.map((entry) => sdk.ack(entry.id)));
274
+ success(result);
275
+ } catch (error) {
276
+ handleSdkError(error);
163
277
  }
164
278
  });
165
279
  }
166
280
  //#endregion
167
281
  //#region src/commands/client.ts
168
282
  function registerClientCommands(program) {
169
- const client = program.command("client").description("Manage First Tree Hub client");
283
+ const client = program.command("client").description("Client runtime connect agents to the server");
170
284
  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) => {
171
285
  try {
172
286
  await promptMissingFields({
@@ -184,7 +298,7 @@ function registerClientCommands(program) {
184
298
  });
185
299
  if (agents.size === 0) {
186
300
  process.stderr.write(" No agents configured.\n");
187
- process.stderr.write(" Add one with: first-tree-hub client add <name> --token <token>\n");
301
+ process.stderr.write(" Add one with: first-tree-hub agent add <name> --token <token>\n");
188
302
  process.exit(1);
189
303
  }
190
304
  process.stderr.write(`\n Connecting to ${config.server.url}...\n`);
@@ -244,91 +358,6 @@ function registerClientCommands(program) {
244
358
  process.stderr.write(" No agents directory found.\n");
245
359
  }
246
360
  });
247
- client.command("add [name]").description("Add an agent instance").option("-t, --token <token>", "Agent token").action(async (name, options) => {
248
- try {
249
- let agentName = name;
250
- let agentToken = options?.token;
251
- if (!agentName || !agentToken) {
252
- const result = await promptAddAgent();
253
- agentName = agentName ?? result.name;
254
- agentToken = agentToken ?? result.token;
255
- }
256
- const agentDir = join(DEFAULT_CONFIG_DIR, "agents", agentName);
257
- mkdirSync(agentDir, {
258
- recursive: true,
259
- mode: 448
260
- });
261
- setConfigValue(join(agentDir, "agent.yaml"), "token", agentToken);
262
- process.stderr.write(` Agent "${agentName}" added.\n`);
263
- process.stderr.write(` Config: ${join(agentDir, "agent.yaml")}\n`);
264
- } catch (error) {
265
- if (error.name === "ExitPromptError") {
266
- process.stderr.write("\n Cancelled.\n");
267
- return;
268
- }
269
- const msg = error instanceof Error ? error.message : String(error);
270
- process.stderr.write(` Error: ${msg}\n`);
271
- process.exit(1);
272
- }
273
- });
274
- client.command("remove <name>").description("Remove an agent instance and its runtime data").action((name) => {
275
- const agentDir = join(DEFAULT_CONFIG_DIR, "agents", name);
276
- if (!existsSync(agentDir)) {
277
- process.stderr.write(` Agent "${name}" not found.\n`);
278
- process.exit(1);
279
- }
280
- rmSync(agentDir, {
281
- recursive: true,
282
- force: true
283
- });
284
- rmSync(join(DEFAULT_DATA_DIR, "workspaces", name), {
285
- recursive: true,
286
- force: true
287
- });
288
- rmSync(join(DEFAULT_DATA_DIR, "sessions", `${name}.json`), { force: true });
289
- process.stderr.write(` Agent "${name}" removed.\n`);
290
- });
291
- client.command("list").description("List configured agents").action(() => {
292
- const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
293
- try {
294
- const agents = loadAgents({
295
- schema: agentConfigSchema,
296
- agentsDir
297
- });
298
- if (agents.size === 0) {
299
- process.stderr.write(" No agents configured.\n");
300
- return;
301
- }
302
- for (const [name, config] of agents) {
303
- const masked = config.token.length > 8 ? `${config.token.slice(0, 6)}***${config.token.slice(-2)}` : "***";
304
- process.stderr.write(` ${name.padEnd(20)} type: ${config.type.padEnd(14)} token: ${masked}\n`);
305
- }
306
- } catch {
307
- process.stderr.write(" No agents configured.\n");
308
- }
309
- });
310
- client.command("workspace").description("Manage agent workspaces").command("clean [agent-name]").description("Remove stale workspace directories (older than TTL with no active session)").option("--ttl <days>", "TTL in days", String(DEFAULT_WORKSPACE_TTL_MS / (1440 * 60 * 1e3))).action((agentName, options) => {
311
- const defaultDays = DEFAULT_WORKSPACE_TTL_MS / (1440 * 60 * 1e3);
312
- const ttlMs = Number.parseInt(options?.ttl ?? String(defaultDays), 10) * 24 * 60 * 60 * 1e3;
313
- const workspacesDir = join(DEFAULT_DATA_DIR, "workspaces");
314
- if (!existsSync(workspacesDir)) {
315
- process.stderr.write(" No workspaces found.\n");
316
- return;
317
- }
318
- const agentNames = agentName ? [agentName] : readdirSync(workspacesDir);
319
- let totalRemoved = 0;
320
- for (const name of agentNames) {
321
- const agentWorkspaceRoot = join(workspacesDir, name);
322
- if (!existsSync(agentWorkspaceRoot)) continue;
323
- const persisted = new SessionRegistry(join(DEFAULT_DATA_DIR, "sessions", `${name}.json`)).load();
324
- const activeChatIds = /* @__PURE__ */ new Set();
325
- for (const [chatId, data] of persisted) if (data.status !== "evicted") activeChatIds.add(chatId);
326
- const removed = cleanWorkspaces(agentWorkspaceRoot, activeChatIds, ttlMs);
327
- totalRemoved += removed.length;
328
- for (const chatId of removed) process.stderr.write(` Removed: ${name}/${chatId}\n`);
329
- }
330
- process.stderr.write(` ${totalRemoved} workspace(s) cleaned.\n`);
331
- });
332
361
  }
333
362
  //#endregion
334
363
  //#region src/commands/config.ts
@@ -424,43 +453,11 @@ function isSecretField(schema, dotPath) {
424
453
  return false;
425
454
  }
426
455
  //#endregion
427
- //#region src/commands/db.ts
428
- function registerDbCommands(program) {
429
- program.command("db").description("Database management").command("migrate").description("Run database migrations").action(async () => {
430
- try {
431
- const tableCount = await runMigrations((await initConfig({
432
- schema: serverConfigSchema,
433
- role: "server"
434
- })).database.url);
435
- process.stderr.write(` Migrations complete (${tableCount} tables)\n`);
436
- } catch (error) {
437
- const msg = error instanceof Error ? error.message : String(error);
438
- process.stderr.write(` Error: ${msg}\n`);
439
- process.exit(1);
440
- }
441
- });
442
- }
443
- //#endregion
444
- //#region src/commands/history.ts
445
- function registerHistoryCommand(program) {
446
- program.command("history <chatId>").description("View message history in a chat").option("-l, --limit <number>", "Maximum messages to return (1-100)", "20").option("--cursor <cursor>", "Pagination cursor from previous response").action(async (chatId, options) => {
447
- try {
448
- const limit = parseLimit(options.limit, 100);
449
- success(await createSdk().listMessages(chatId, {
450
- limit,
451
- cursor: options.cursor
452
- }));
453
- } catch (error) {
454
- handleError(error);
455
- }
456
- });
457
- }
458
- //#endregion
459
456
  //#region src/commands/onboard.ts
460
457
  async function promptMissing(args) {
461
458
  let ghUsername = null;
462
459
  try {
463
- const { getGitHubUsername } = await import("../bootstrap-waEJYq3g.mjs").then((n) => n.n);
460
+ const { getGitHubUsername } = await import("../bootstrap-CPdLNPme.mjs").then((n) => n.n);
464
461
  ghUsername = getGitHubUsername();
465
462
  } catch {}
466
463
  if (!args.id) {
@@ -518,7 +515,7 @@ async function promptMissing(args) {
518
515
  }
519
516
  }
520
517
  if (!args.server) try {
521
- const { resolveServerUrl } = await import("../bootstrap-waEJYq3g.mjs").then((n) => n.n);
518
+ const { resolveServerUrl } = await import("../bootstrap-CPdLNPme.mjs").then((n) => n.n);
522
519
  resolveServerUrl();
523
520
  } catch {
524
521
  args.server = await input({ message: "Hub server URL:" });
@@ -592,60 +589,6 @@ function registerOnboardCommand(program) {
592
589
  });
593
590
  }
594
591
  //#endregion
595
- //#region src/commands/send.ts
596
- const MAX_STDIN_BYTES = 10 * 1024 * 1024;
597
- /** Read all of stdin as a string. Returns null if stdin is a TTY. */
598
- function readStdin() {
599
- if (process.stdin.isTTY) return Promise.resolve(null);
600
- return new Promise((resolve, reject) => {
601
- const chunks = [];
602
- let totalSize = 0;
603
- process.stdin.on("data", (chunk) => {
604
- totalSize += chunk.length;
605
- if (totalSize > MAX_STDIN_BYTES) {
606
- process.stdin.destroy();
607
- reject(/* @__PURE__ */ new Error(`stdin exceeds ${MAX_STDIN_BYTES} bytes`));
608
- return;
609
- }
610
- chunks.push(chunk);
611
- });
612
- process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
613
- process.stdin.on("error", reject);
614
- });
615
- }
616
- function registerSendCommand(program) {
617
- program.command("send <target> [message]").description("Send a message to an agent or chat").option("-f, --format <format>", "Message format (text|markdown|card)", "text").option("--chat", "Treat target as chat ID instead of agent ID").option("-m, --metadata <json>", "JSON metadata to attach").option("--reply-to <messageId>", "Message ID to reply to").option("--reply-to-inbox <inboxId>", "Cross-chat reply target inbox").option("--reply-to-chat <chatId>", "Cross-chat reply target chat").action(async (target, message, options) => {
618
- try {
619
- const content = message ?? await readStdin();
620
- if (!content) fail("NO_MESSAGE", "No message provided. Pass as argument or pipe via stdin.", 2);
621
- let metadata;
622
- if (options.metadata) try {
623
- metadata = JSON.parse(options.metadata);
624
- } catch {
625
- fail("INVALID_METADATA", "Metadata must be valid JSON.", 2);
626
- }
627
- const sdk = createSdk();
628
- if (options.chat) success(await sdk.sendMessage(target, {
629
- format: options.format,
630
- content,
631
- metadata,
632
- inReplyTo: options.replyTo,
633
- replyToInbox: options.replyToInbox,
634
- replyToChat: options.replyToChat
635
- }));
636
- else success(await sdk.sendToAgent(target, {
637
- format: options.format,
638
- content,
639
- metadata,
640
- replyToInbox: options.replyToInbox,
641
- replyToChat: options.replyToChat
642
- }));
643
- } catch (error) {
644
- handleError(error);
645
- }
646
- });
647
- }
648
- //#endregion
649
592
  //#region src/commands/server.ts
650
593
  function registerServerCommands(program) {
651
594
  const server = program.command("server").description("Manage First Tree Hub server");
@@ -662,8 +605,14 @@ function registerServerCommands(program) {
662
605
  }
663
606
  });
664
607
  server.command("stop").description("Stop the managed PostgreSQL container").action(() => {
665
- if (stopPostgres()) process.stderr.write(" PostgreSQL container stopped.\n");
666
- else process.stderr.write(" No managed PostgreSQL container found.\n");
608
+ try {
609
+ if (stopPostgres()) process.stderr.write(" PostgreSQL container stopped.\n");
610
+ else process.stderr.write(" No managed PostgreSQL container found.\n");
611
+ } catch (error) {
612
+ const msg = error instanceof Error ? error.message : String(error);
613
+ process.stderr.write(` Error stopping PostgreSQL: ${msg}\n`);
614
+ process.exit(1);
615
+ }
667
616
  });
668
617
  server.command("doctor").description("Check server environment readiness").action(async () => {
669
618
  process.stderr.write("\n First Tree Hub Server Doctor\n\n");
@@ -693,6 +642,33 @@ function registerServerCommands(program) {
693
642
  process.exit(1);
694
643
  }
695
644
  });
645
+ server.command("db:migrate").description("Run database migrations").action(async () => {
646
+ try {
647
+ const tableCount = await runMigrations((await initConfig({
648
+ schema: serverConfigSchema,
649
+ role: "server"
650
+ })).database.url);
651
+ process.stderr.write(` Migrations complete (${tableCount} tables)\n`);
652
+ } catch (error) {
653
+ const msg = error instanceof Error ? error.message : String(error);
654
+ process.stderr.write(` Error: ${msg}\n`);
655
+ process.exit(1);
656
+ }
657
+ });
658
+ server.command("admin:create").description("Create an admin user").option("-u, --username <name>", "Admin username", "admin").option("-p, --password <pass>", "Admin password (auto-generated if omitted)").action(async (options) => {
659
+ try {
660
+ const result = await createAdminUser((await initConfig({
661
+ schema: serverConfigSchema,
662
+ role: "server"
663
+ })).database.url, options.username, options.password);
664
+ process.stderr.write(` Admin user "${result.username}" created.\n`);
665
+ if (!options.password) process.stderr.write(` Password: ${result.password} (save this — shown only once)\n`);
666
+ } catch (error) {
667
+ const msg = error instanceof Error ? error.message : String(error);
668
+ process.stderr.write(` Error: ${msg}\n`);
669
+ process.exit(1);
670
+ }
671
+ });
696
672
  }
697
673
  //#endregion
698
674
  //#region src/commands/status.ts
@@ -752,102 +728,16 @@ function formatUptime(seconds) {
752
728
  return `${mins}m`;
753
729
  }
754
730
  //#endregion
755
- //#region src/commands/token.ts
756
- function registerTokenCommands(program) {
757
- program.command("token").description("Agent token management").command("bootstrap <agentId>").description("Bootstrap a token using GitHub identity (requires gh CLI)").option("--save-to <target>", "Save token to: \"agent\" (default) or a file path", "agent").option("--server <url>", "Hub server URL").action(async (agentId, options) => {
758
- try {
759
- const result = await bootstrapToken(resolveServerUrl(options.server), agentId, { saveTo: options.saveTo });
760
- if (options.saveTo === "agent") process.stderr.write(`Token saved to ~/.first-tree-hub/agents/${agentId}/agent.yaml\n`);
761
- else process.stderr.write(`Token saved to ${options.saveTo}\n`);
762
- success({
763
- agentId: result.agentId,
764
- tokenSaved: true
765
- });
766
- } catch (error) {
767
- fail("BOOTSTRAP_ERROR", error instanceof Error ? error.message : String(error));
768
- }
769
- });
770
- }
771
- //#endregion
772
- //#region src/cli/connect.ts
773
- function registerConnectCommand(program) {
774
- program.command("connect").description("Connect a single agent to server and process messages").option("-t, --type <type>", "Handler type", "claude-code").option("--concurrency <n>", "Max parallel message processing", "5").option("--server <url>", "Override FIRST_TREE_HUB_SERVER").action(async (options) => {
775
- try {
776
- registerBuiltinHandlers();
777
- const config = resolveConfig();
778
- if (options.server) config.serverUrl = options.server;
779
- const concurrency = Number.parseInt(options.concurrency, 10) || 5;
780
- const handlerFactory = getHandlerFactory(options.type);
781
- const slot = new AgentSlot({
782
- name: "connect",
783
- serverUrl: config.serverUrl,
784
- token: config.token,
785
- type: options.type,
786
- handlerFactory,
787
- session: {
788
- idle_timeout: 300,
789
- max_sessions: 10
790
- },
791
- concurrency
792
- });
793
- const agent = await slot.start();
794
- log("connect", `Registered as ${agent.displayName ?? agent.agentId} (${agent.agentId})`);
795
- const shutdown = async () => {
796
- log("connect", "Shutting down...");
797
- await slot.stop();
798
- process.exit(0);
799
- };
800
- process.on("SIGINT", () => void shutdown());
801
- process.on("SIGTERM", () => void shutdown());
802
- } catch (error) {
803
- handleError(error);
804
- }
805
- });
806
- }
807
- //#endregion
808
- //#region src/cli/start.ts
809
- function registerStartCommand(program) {
810
- program.command("start").description("Start the Agent Runtime — manage multiple agents from a config file").option("-c, --config <path>", "Path to agents.yaml config file", "./agents.yaml").option("--server <url>", "Override server URL from config").option("--shutdown-timeout <ms>", "Graceful shutdown timeout in ms", "30000").action(async (options) => {
811
- try {
812
- registerBuiltinHandlers();
813
- log("runtime", `Loading config from ${options.config}`);
814
- const config = loadRuntimeConfig(options.config);
815
- if (options.server) config.server = options.server;
816
- const shutdownTimeout = Number.parseInt(options.shutdownTimeout, 10);
817
- if (Number.isNaN(shutdownTimeout) || shutdownTimeout < 0) fail("INVALID_OPTION", "shutdown-timeout must be a positive number", 2);
818
- await new AgentRuntime({
819
- config,
820
- shutdownTimeout
821
- }).start();
822
- } catch (error) {
823
- if (error instanceof Error) {
824
- log("runtime", `Fatal: ${error.message}`);
825
- process.exit(1);
826
- }
827
- throw error;
828
- }
829
- });
830
- }
831
- //#endregion
832
731
  //#region src/cli/index.ts
833
732
  const { version } = createRequire(import.meta.url)("../../package.json");
834
733
  const program = new Command();
835
734
  program.name("first-tree-hub").description("First Tree Hub — centralized collaboration platform for agent teams").version(version);
836
735
  registerServerCommands(program);
837
736
  registerClientCommands(program);
838
- registerDbCommands(program);
839
- registerAdminCommands(program);
737
+ registerAgentCommands(program);
840
738
  registerConfigCommands(program);
841
739
  registerStatusCommand(program);
842
- registerConnectCommand(program);
843
- registerStartCommand(program);
844
- registerAgentCommands(program);
845
740
  registerOnboardCommand(program);
846
- registerTokenCommands(program);
847
- registerBindCommands(program);
848
- registerSendCommand(program);
849
- registerChatsCommand(program);
850
- registerHistoryCommand(program);
851
741
  program.parse();
852
742
  //#endregion
853
743
  export {};
@@ -1,18 +1,18 @@
1
- import { a as getGitHubUsername, b as serverConfigSchema, c as DEFAULT_CONFIG_DIR, d as clientConfigSchema, f as collectMissingPrompts, h as loadAgents, m as initConfig, r as checkBootstrapStatus, s as resolveServerUrl, t as bootstrapToken$1, u as agentConfigSchema, x as setConfigValue, y as resolveConfigReadonly } from "./bootstrap-waEJYq3g.mjs";
2
- import { ZodError, z } from "zod";
1
+ import { a as getGitHubUsername, b as serverConfigSchema, c as DEFAULT_CONFIG_DIR, d as clientConfigSchema, f as collectMissingPrompts, h as loadAgents, m as initConfig, r as checkBootstrapStatus, s as resolveServerUrl, t as bootstrapToken$1, u as agentConfigSchema, x as setConfigValue, y as resolveConfigReadonly } from "./bootstrap-CPdLNPme.mjs";
3
2
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
3
  import { dirname, join, resolve } from "node:path";
5
- import { parse } from "yaml";
4
+ import { EventEmitter } from "node:events";
5
+ import WebSocket from "ws";
6
6
  import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
7
+ import { query } from "@anthropic-ai/claude-agent-sdk";
8
+ import { execFileSync, execSync } from "node:child_process";
9
+ import { ZodError, z } from "zod";
10
+ import "yaml";
7
11
  import { homedir } from "node:os";
8
12
  import bcrypt from "bcrypt";
9
13
  import { and, desc, eq, gt, inArray, isNotNull, isNull, lt, ne, sql } from "drizzle-orm";
10
14
  import { drizzle } from "drizzle-orm/postgres-js";
11
15
  import postgres from "postgres";
12
- import { execFileSync, execSync } from "node:child_process";
13
- import { EventEmitter } from "node:events";
14
- import WebSocket from "ws";
15
- import { query } from "@anthropic-ai/claude-agent-sdk";
16
16
  import { fileURLToPath } from "node:url";
17
17
  import { migrate } from "drizzle-orm/postgres-js/migrator";
18
18
  import { input, password, select } from "@inquirer/prompts";
@@ -24,40 +24,6 @@ import Fastify from "fastify";
24
24
  import { bigserial, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique } from "drizzle-orm/pg-core";
25
25
  import { SignJWT, jwtVerify } from "jose";
26
26
  import { Client, EventDispatcher, LoggerLevel, WSClient } from "@larksuiteoapi/node-sdk";
27
- //#region src/core/admin.ts
28
- /**
29
- * Check if any admin user exists.
30
- */
31
- async function hasAdminUser(databaseUrl) {
32
- const client = postgres(databaseUrl, { max: 1 });
33
- try {
34
- return (await client`SELECT count(*)::int AS count FROM admin_users`)[0].count > 0;
35
- } finally {
36
- await client.end();
37
- }
38
- }
39
- /**
40
- * Create an admin user. Returns the generated password.
41
- */
42
- async function createAdminUser$1(databaseUrl, username, password) {
43
- const pw = password ?? randomBytes(12).toString("base64url");
44
- const hash = await bcrypt.hash(pw, 12);
45
- const client = postgres(databaseUrl, { max: 1 });
46
- const db = drizzle(client);
47
- try {
48
- await db.execute(sql`
49
- INSERT INTO admin_users (id, username, password_hash, role)
50
- VALUES (${randomUUID()}, ${username}, ${hash}, 'super_admin')
51
- `);
52
- } finally {
53
- await client.end();
54
- }
55
- return {
56
- username,
57
- password: pw
58
- };
59
- }
60
- //#endregion
61
27
  //#region ../client/dist/index.mjs
62
28
  const FETCH_TIMEOUT_MS = 15e3;
63
29
  var FirstTreeHubSDK = class {
@@ -496,109 +462,7 @@ defineConfig({
496
462
  webhookMax: field(z.number().default(60), { env: "FIRST_TREE_HUB_RATE_LIMIT_WEBHOOK_MAX" })
497
463
  })
498
464
  });
499
- const CONTEXT_TREE_DIR$1 = join(DEFAULT_DATA_DIR, "context-tree");
500
- /**
501
- * Sync the shared Context Tree git clone.
502
- *
503
- * Clones on first run, pulls on subsequent runs.
504
- * Returns the clone path on success, null on failure (graceful degradation).
505
- */
506
- async function syncContextTree(serverUrl, token, log) {
507
- try {
508
- execFileSync("git", ["--version"], { stdio: "ignore" });
509
- } catch {
510
- log("Context Tree sync skipped: git is not installed");
511
- return null;
512
- }
513
- let repo;
514
- let branch;
515
- try {
516
- const config = await new FirstTreeHubSDK({
517
- serverUrl,
518
- token
519
- }).getContextTreeConfig();
520
- repo = config.repo;
521
- branch = config.branch;
522
- } catch (err) {
523
- log(`Context Tree sync skipped: failed to fetch config from server (${err instanceof Error ? err.message : String(err)})`);
524
- return null;
525
- }
526
- try {
527
- if (existsSync(join(CONTEXT_TREE_DIR$1, ".git"))) {
528
- if (execFileSync("git", [
529
- "rev-parse",
530
- "--abbrev-ref",
531
- "HEAD"
532
- ], {
533
- cwd: CONTEXT_TREE_DIR$1,
534
- encoding: "utf-8",
535
- timeout: 5e3
536
- }).trim() !== branch) {
537
- execFileSync("git", ["checkout", branch], {
538
- cwd: CONTEXT_TREE_DIR$1,
539
- stdio: "pipe",
540
- timeout: 1e4
541
- });
542
- log(`Context Tree switched to branch ${branch}`);
543
- }
544
- execFileSync("git", ["pull", "--ff-only"], {
545
- cwd: CONTEXT_TREE_DIR$1,
546
- stdio: "pipe",
547
- timeout: 3e4
548
- });
549
- log(`Context Tree updated (pull)`);
550
- } else {
551
- mkdirSync(CONTEXT_TREE_DIR$1, { recursive: true });
552
- execFileSync("git", [
553
- "clone",
554
- "--branch",
555
- branch,
556
- "--single-branch",
557
- repo,
558
- CONTEXT_TREE_DIR$1
559
- ], {
560
- stdio: "pipe",
561
- timeout: 6e4
562
- });
563
- log(`Context Tree cloned from ${repo} (branch: ${branch})`);
564
- }
565
- return CONTEXT_TREE_DIR$1;
566
- } catch (err) {
567
- const msg = err instanceof Error ? err.message : String(err);
568
- log(`Context Tree sync failed: ${msg}`);
569
- log("Check that git credentials (SSH key or credential helper) are configured for this repo");
570
- if ((msg.includes("cannot fast-forward") || msg.includes("not possible to fast-forward") || msg.includes("CONFLICT")) && existsSync(join(CONTEXT_TREE_DIR$1, ".git"))) {
571
- log("Diverged history detected, attempting fresh clone...");
572
- try {
573
- rmSync(CONTEXT_TREE_DIR$1, {
574
- recursive: true,
575
- force: true
576
- });
577
- mkdirSync(CONTEXT_TREE_DIR$1, { recursive: true });
578
- execFileSync("git", [
579
- "clone",
580
- "--branch",
581
- branch,
582
- "--single-branch",
583
- repo,
584
- CONTEXT_TREE_DIR$1
585
- ], {
586
- stdio: "pipe",
587
- timeout: 6e4
588
- });
589
- log("Context Tree re-cloned successfully");
590
- return CONTEXT_TREE_DIR$1;
591
- } catch {
592
- log("Context Tree re-clone also failed, continuing without context");
593
- }
594
- }
595
- if (existsSync(join(CONTEXT_TREE_DIR$1, ".git"))) {
596
- log("Using existing Context Tree clone despite sync failure");
597
- return CONTEXT_TREE_DIR$1;
598
- }
599
- return null;
600
- }
601
- }
465
+ join(DEFAULT_DATA_DIR, "context-tree");
602
466
  /**
603
467
  * Bootstrap a workspace with .agent/ directory files.
604
468
  *
@@ -1454,92 +1318,44 @@ const agentSlotConfigSchema = z.object({
1454
1318
  session: sessionConfigSchema.prefault({}),
1455
1319
  concurrency: z.number().int().positive().default(5)
1456
1320
  });
1457
- const runtimeConfigSchema = z.object({
1321
+ z.object({
1458
1322
  server: z.url().default("http://localhost:8000"),
1459
1323
  agents: z.record(z.string(), agentSlotConfigSchema).refine((agents) => Object.keys(agents).length > 0, "At least one agent must be defined")
1460
1324
  });
1461
- function expandEnvVars(value) {
1462
- return value.replace(/\$\{([^}]+)\}/g, (_, name) => {
1463
- const envVal = process.env[name];
1464
- if (envVal === void 0) throw new Error(`Environment variable "${name}" is not set`);
1465
- return envVal;
1466
- });
1467
- }
1468
- function deepExpandEnv(obj) {
1469
- if (typeof obj === "string") return expandEnvVars(obj);
1470
- if (Array.isArray(obj)) return obj.map(deepExpandEnv);
1471
- if (obj !== null && typeof obj === "object") {
1472
- const result = {};
1473
- for (const [key, value] of Object.entries(obj)) result[key] = deepExpandEnv(value);
1474
- return result;
1325
+ //#endregion
1326
+ //#region src/core/admin.ts
1327
+ /**
1328
+ * Check if any admin user exists.
1329
+ */
1330
+ async function hasAdminUser(databaseUrl) {
1331
+ const client = postgres(databaseUrl, { max: 1 });
1332
+ try {
1333
+ return (await client`SELECT count(*)::int AS count FROM admin_users`)[0].count > 0;
1334
+ } finally {
1335
+ await client.end();
1475
1336
  }
1476
- return obj;
1477
1337
  }
1478
- function loadRuntimeConfig(configPath) {
1479
- const expanded = deepExpandEnv(parse(readFileSync(configPath, "utf-8")));
1480
- return runtimeConfigSchema.parse(expanded);
1481
- }
1482
- const DEFAULT_SHUTDOWN_TIMEOUT = 3e4;
1483
- var AgentRuntime = class {
1484
- slots = [];
1485
- config;
1486
- shutdownTimeout;
1487
- stopping = false;
1488
- constructor(options) {
1489
- this.config = options.config;
1490
- this.shutdownTimeout = options.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT;
1491
- for (const [name, agentConfig] of Object.entries(this.config.agents)) {
1492
- const handlerFactory = getHandlerFactory(agentConfig.type);
1493
- this.slots.push(new AgentSlot({
1494
- name,
1495
- serverUrl: this.config.server,
1496
- token: agentConfig.token,
1497
- type: agentConfig.type,
1498
- handlerFactory,
1499
- session: agentConfig.session,
1500
- concurrency: agentConfig.concurrency
1501
- }));
1502
- }
1503
- }
1504
- /** Start all agent slots and block until shutdown signal. */
1505
- async start() {
1506
- const log = (msg) => process.stderr.write(`[runtime] ${msg}\n`);
1507
- const firstToken = Object.values(this.config.agents)[0]?.token;
1508
- let contextTreePath = null;
1509
- if (firstToken) contextTreePath = await syncContextTree(this.config.server, firstToken, log);
1510
- if (!contextTreePath) log("WARNING: Context Tree sync failed — agents will start without organizational context");
1511
- log(`Starting ${this.slots.length} agent(s)...`);
1512
- const results = await Promise.allSettled(this.slots.map((slot) => slot.start(contextTreePath)));
1513
- let failed = 0;
1514
- for (const result of results) if (result.status === "rejected") {
1515
- log(`Failed to start agent: ${result.reason instanceof Error ? result.reason.message : result.reason}`);
1516
- failed++;
1517
- }
1518
- if (failed === this.slots.length) throw new Error("All agents failed to start");
1519
- log("Ready. Press Ctrl+C to stop.");
1520
- await new Promise((resolve) => {
1521
- const shutdown = async () => {
1522
- if (this.stopping) return;
1523
- this.stopping = true;
1524
- log("Shutting down...");
1525
- const timer = setTimeout(() => {
1526
- log("Shutdown timeout reached, forcing exit");
1527
- process.exit(1);
1528
- }, this.shutdownTimeout);
1529
- await this.stop();
1530
- clearTimeout(timer);
1531
- log("Stopped");
1532
- resolve();
1533
- };
1534
- process.on("SIGINT", shutdown);
1535
- process.on("SIGTERM", shutdown);
1536
- });
1537
- }
1538
- /** Stop all slots. */
1539
- async stop() {
1540
- await Promise.allSettled(this.slots.map((slot) => slot.stop()));
1338
+ /**
1339
+ * Create an admin user. Returns the generated password.
1340
+ */
1341
+ async function createAdminUser$1(databaseUrl, username, password) {
1342
+ const pw = password ?? randomBytes(12).toString("base64url");
1343
+ const hash = await bcrypt.hash(pw, 12);
1344
+ const client = postgres(databaseUrl, { max: 1 });
1345
+ const db = drizzle(client);
1346
+ try {
1347
+ await db.execute(sql`
1348
+ INSERT INTO admin_users (id, username, password_hash, role)
1349
+ VALUES (${randomUUID()}, ${username}, ${hash}, 'super_admin')
1350
+ `);
1351
+ } finally {
1352
+ await client.end();
1541
1353
  }
1542
- };
1354
+ return {
1355
+ username,
1356
+ password: pw
1357
+ };
1358
+ }
1543
1359
  //#endregion
1544
1360
  //#region src/core/client-runtime.ts
1545
1361
  /**
@@ -2564,13 +2380,16 @@ async function onboardContinue(args) {
2564
2380
  process.stderr.write("\n✅ Onboard complete!\n\n");
2565
2381
  process.stderr.write(` Human: ${mergedArgs.id}\n`);
2566
2382
  if (mergedArgs.assistant) process.stderr.write(` Assistant: ${mergedArgs.assistant}\n`);
2567
- process.stderr.write(` Token: ~/.first-tree-hub/agents/${agentToBootstrap}/agent.yaml\n`);
2383
+ process.stderr.write(` Token: ~/.first-tree-hub/config/agents/${agentToBootstrap}/agent.yaml\n`);
2568
2384
  if (mergedArgs.feishuBotAppId) process.stderr.write(` Feishu: bot bound (${mergedArgs.feishuBotAppId})\n`);
2385
+ setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", serverUrl);
2569
2386
  if (mergedArgs.type === "human") {
2570
2387
  process.stderr.write("\n Next step — bind your Feishu account:\n");
2571
2388
  process.stderr.write(` Send this message to the bot in Feishu: /bind ${mergedArgs.id}\n`);
2572
2389
  if (!mergedArgs.feishuBotAppId) process.stderr.write(" (requires a Feishu bot to be configured in the system)\n");
2573
2390
  }
2391
+ process.stderr.write("\n Start the agent:\n");
2392
+ process.stderr.write(" first-tree-hub client start\n");
2574
2393
  process.stderr.write("\n");
2575
2394
  }
2576
2395
  function createMemberNodeMd(repoPath, data) {
@@ -6594,4 +6413,4 @@ function resolveWebDist() {
6594
6413
  } catch {}
6595
6414
  }
6596
6415
  //#endregion
6597
- export { ClientRuntime as A, registerBuiltinHandlers as B, checkWebSocket as C, ensurePostgres as D, status as E, SdkError as F, hasAdminUser as H, SessionRegistry as I, cleanWorkspaces as L, AgentSlot as M, DEFAULT_WORKSPACE_TTL_MS as N, isDockerAvailable as O, FirstTreeHubSDK as P, getHandlerFactory as R, checkServerReachable as S, blank as T, createAdminUser$1 as V, checkDocker as _, formatCheckReport as a, checkServerConfig as b, onboardContinue as c, runMigrations as d, checkAgentConfigs as f, checkDatabase as g, checkContextTreeRepo as h, promptMissingFields as i, AgentRuntime as j, stopPostgres as k, onboardCreate as l, checkClientConfig as m, isInteractive as n, loadOnboardState as o, checkAgentTokens as p, promptAddAgent as r, onboardCheck as s, startServer as t, saveOnboardState as u, checkGitHubToken as v, printResults as w, checkServerHealth as x, checkNodeVersion as y, loadRuntimeConfig as z };
6416
+ export { ClientRuntime as A, checkWebSocket as C, ensurePostgres as D, status as E, SessionRegistry as F, cleanWorkspaces as I, hasAdminUser as M, FirstTreeHubSDK as N, isDockerAvailable as O, SdkError as P, checkServerReachable as S, blank as T, checkDocker as _, formatCheckReport as a, checkServerConfig as b, onboardContinue as c, runMigrations as d, checkAgentConfigs as f, checkDatabase as g, checkContextTreeRepo as h, promptMissingFields as i, createAdminUser$1 as j, stopPostgres as k, onboardCreate as l, checkClientConfig as m, isInteractive as n, loadOnboardState as o, checkAgentTokens as p, promptAddAgent as r, onboardCheck as s, startServer as t, saveOnboardState as u, checkGitHubToken as v, printResults as w, checkServerHealth as x, checkNodeVersion as y };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as getGitHubUsername, i as getGitHubToken, o as resolveAgentToken, r as checkBootstrapStatus, s as resolveServerUrl, t as bootstrapToken } from "./bootstrap-waEJYq3g.mjs";
2
- import { A as ClientRuntime, C as checkWebSocket, D as ensurePostgres, E as status, F as SdkError, H as hasAdminUser, O as isDockerAvailable, P as FirstTreeHubSDK, S as checkServerReachable, T as blank, V as createAdminUser, _ as checkDocker, a as formatCheckReport, b as checkServerConfig, c as onboardContinue, d as runMigrations, f as checkAgentConfigs, g as checkDatabase, h as checkContextTreeRepo, i as promptMissingFields, k as stopPostgres, l as onboardCreate, m as checkClientConfig, n as isInteractive, p as checkAgentTokens, r as promptAddAgent, s as onboardCheck, t as startServer, v as checkGitHubToken, w as printResults, x as checkServerHealth, y as checkNodeVersion } from "./core-Bl6djdPd.mjs";
1
+ import { A as ClientRuntime, C as checkWebSocket, D as ensurePostgres, E as status, M as hasAdminUser, N as FirstTreeHubSDK, O as isDockerAvailable, P as SdkError, S as checkServerReachable, T as blank, _ as checkDocker, a as formatCheckReport, b as checkServerConfig, c as onboardContinue, d as runMigrations, f as checkAgentConfigs, g as checkDatabase, h as checkContextTreeRepo, i as promptMissingFields, j as createAdminUser, k as stopPostgres, l as onboardCreate, m as checkClientConfig, n as isInteractive, p as checkAgentTokens, r as promptAddAgent, s as onboardCheck, t as startServer, v as checkGitHubToken, w as printResults, x as checkServerHealth, y as checkNodeVersion } from "./core-CZjUVAU-.mjs";
2
+ import { a as getGitHubUsername, i as getGitHubToken, o as resolveAgentToken, r as checkBootstrapStatus, s as resolveServerUrl, t as bootstrapToken } from "./bootstrap-CPdLNPme.mjs";
3
3
  import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-Y4m2zFc3.mjs";
4
4
  export { ClientRuntime, FirstTreeHubSDK, SdkError, bindFeishuBot, bindFeishuUser, blank, bootstrapToken, checkAgentConfigs, checkAgentTokens, checkBootstrapStatus, checkClientConfig, checkContextTreeRepo, checkDatabase, checkDocker, checkGitHubToken, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createAdminUser, ensurePostgres, formatCheckReport, getGitHubToken, getGitHubUsername, hasAdminUser, isDockerAvailable, isInteractive, onboardCheck, onboardContinue, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAgentToken, resolveServerUrl, runMigrations, startServer, status, stopPostgres };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-team-foundation/first-tree-hub",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "description": "First Tree Hub — unified CLI for server, client, and agent management",
6
6
  "exports": {