@agent-team-foundation/first-tree-hub 0.2.0

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.
@@ -0,0 +1,636 @@
1
+ #!/usr/bin/env node
2
+ import { A as loadRuntimeConfig, B as readConfigFile, C as stopPostgres, D as FirstTreeHubSDK, E as AgentSlot, F as agentConfigSchema, H as resetConfigMeta, I as clientConfigSchema, L as getConfigValue, M as createAdminUser, O as SdkError, P as DEFAULT_CONFIG_DIR, R as initConfig, T as AgentRuntime, U as serverConfigSchema, V as resetConfig, W as setConfigValue, _ as checkWebSocket, a as runMigrations, c as checkClientConfig, d as checkDocker, f as checkGitHubToken, g as checkServerReachable, h as checkServerHealth, i as promptMissingFields, j as registerBuiltinHandlers, k as getHandlerFactory, l as checkContextTreeRepo, m as checkServerConfig, o as checkAgentConfigs, p as checkNodeVersion, r as promptAddAgent, s as checkAgentTokens, t as startServer, u as checkDatabase, v as printResults, w as ClientRuntime, z as loadAgents } from "../core-CD3xEbyB.mjs";
3
+ import { createRequire } from "node:module";
4
+ import { Command } from "commander";
5
+ import { existsSync, mkdirSync, rmSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ //#region src/commands/admin.ts
8
+ function registerAdminCommands(program) {
9
+ 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) => {
10
+ try {
11
+ const result = await createAdminUser((await initConfig({
12
+ schema: serverConfigSchema,
13
+ role: "server"
14
+ })).database.url, options.username, options.password);
15
+ process.stderr.write(` Admin user "${result.username}" created.\n`);
16
+ if (!options.password) process.stderr.write(` Password: ${result.password} (save this — shown only once)\n`);
17
+ } catch (error) {
18
+ const msg = error instanceof Error ? error.message : String(error);
19
+ process.stderr.write(` Error: ${msg}\n`);
20
+ process.exit(1);
21
+ }
22
+ });
23
+ }
24
+ //#endregion
25
+ //#region src/cli/output.ts
26
+ /** Write a success JSON envelope to stdout. */
27
+ function success(data) {
28
+ process.stdout.write(`${JSON.stringify({
29
+ ok: true,
30
+ data
31
+ })}\n`);
32
+ }
33
+ /** Write an error JSON envelope to stderr and exit with the given code. */
34
+ function fail(code, message, exitCode = 1) {
35
+ process.stderr.write(`${JSON.stringify({
36
+ ok: false,
37
+ error: {
38
+ code,
39
+ message
40
+ }
41
+ })}\n`);
42
+ process.exit(exitCode);
43
+ }
44
+ //#endregion
45
+ //#region src/commands/agent.ts
46
+ function resolveAgentConfig() {
47
+ const token = process.env.FIRST_TREE_HUB_TOKEN;
48
+ if (!token) fail("MISSING_TOKEN", "FIRST_TREE_HUB_TOKEN environment variable is required.", 2);
49
+ return {
50
+ serverUrl: process.env.FIRST_TREE_HUB_SERVER ?? "http://localhost:8000",
51
+ token
52
+ };
53
+ }
54
+ function createSdk$1() {
55
+ return new FirstTreeHubSDK(resolveAgentConfig());
56
+ }
57
+ function handleError$1(error) {
58
+ if (error instanceof SdkError) {
59
+ const exitCode = error.statusCode === 401 ? 3 : 1;
60
+ fail(`HTTP_${error.statusCode}`, error.message, exitCode);
61
+ }
62
+ if (error instanceof TypeError && "cause" in error) fail("CONNECTION_ERROR", `Cannot connect to server: ${error.message}`, 6);
63
+ fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), 1);
64
+ }
65
+ function registerAgentCommands(program) {
66
+ program.command("register").description("Register this agent and return identity info").action(async () => {
67
+ try {
68
+ success(await createSdk$1().register());
69
+ } catch (error) {
70
+ handleError$1(error);
71
+ }
72
+ });
73
+ 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) => {
74
+ try {
75
+ const sdk = createSdk$1();
76
+ const limit = Number.parseInt(options.limit, 10);
77
+ if (Number.isNaN(limit) || limit < 1 || limit > 50) fail("INVALID_LIMIT", "Limit must be between 1 and 50.", 2);
78
+ const result = await sdk.pull(limit);
79
+ if (options.ack && result.entries.length > 0) await Promise.all(result.entries.map((entry) => sdk.ack(entry.id)));
80
+ success(result);
81
+ } catch (error) {
82
+ handleError$1(error);
83
+ }
84
+ });
85
+ }
86
+ //#endregion
87
+ //#region src/cli/util.ts
88
+ function resolveConfig() {
89
+ const token = process.env.FIRST_TREE_HUB_TOKEN;
90
+ if (!token) fail("MISSING_TOKEN", "FIRST_TREE_HUB_TOKEN environment variable is required.", 2);
91
+ return {
92
+ serverUrl: process.env.FIRST_TREE_HUB_SERVER ?? "http://localhost:8000",
93
+ token
94
+ };
95
+ }
96
+ function createSdk() {
97
+ return new FirstTreeHubSDK(resolveConfig());
98
+ }
99
+ function handleError(error) {
100
+ if (error instanceof SdkError) {
101
+ const exitCode = error.statusCode === 401 ? 3 : 1;
102
+ fail(`HTTP_${error.statusCode}`, error.message, exitCode);
103
+ }
104
+ if (error instanceof TypeError && "cause" in error) fail("CONNECTION_ERROR", `Cannot connect to server: ${error.message}`, 6);
105
+ fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), 1);
106
+ }
107
+ /** Parse and validate a numeric limit option from Commander string. */
108
+ function parseLimit(value, max) {
109
+ const limit = Number.parseInt(value, 10);
110
+ if (Number.isNaN(limit) || limit < 1 || limit > max) fail("INVALID_LIMIT", `Limit must be between 1 and ${max}.`, 2);
111
+ return limit;
112
+ }
113
+ /** Write a log line to stderr. */
114
+ function log(tag, message) {
115
+ process.stderr.write(`[${tag}] ${message}\n`);
116
+ }
117
+ //#endregion
118
+ //#region src/commands/chats.ts
119
+ function registerChatsCommand(program) {
120
+ 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) => {
121
+ try {
122
+ const limit = parseLimit(options.limit, 100);
123
+ success(await createSdk().listChats({
124
+ limit,
125
+ cursor: options.cursor
126
+ }));
127
+ } catch (error) {
128
+ handleError(error);
129
+ }
130
+ });
131
+ }
132
+ //#endregion
133
+ //#region src/commands/client.ts
134
+ function registerClientCommands(program) {
135
+ const client = program.command("client").description("Manage First Tree Hub client");
136
+ 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) => {
137
+ try {
138
+ await promptMissingFields({
139
+ schema: clientConfigSchema,
140
+ role: "client",
141
+ noInteractive: options.interactive === false
142
+ });
143
+ const config = await initConfig({
144
+ schema: clientConfigSchema,
145
+ role: "client"
146
+ });
147
+ const agents = loadAgents({
148
+ schema: agentConfigSchema,
149
+ agentsDir: join(DEFAULT_CONFIG_DIR, "agents")
150
+ });
151
+ if (agents.size === 0) {
152
+ process.stderr.write(" No agents configured.\n");
153
+ process.stderr.write(" Add one with: first-tree-hub client add <name> --token <token>\n");
154
+ process.exit(1);
155
+ }
156
+ process.stderr.write(`\n Connecting to ${config.server.url}...\n`);
157
+ const runtime = new ClientRuntime(config.server.url);
158
+ for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
159
+ await runtime.start();
160
+ const shutdown = async () => {
161
+ process.stderr.write("\n Shutting down...\n");
162
+ await runtime.stop();
163
+ process.exit(0);
164
+ };
165
+ process.on("SIGINT", () => void shutdown());
166
+ process.on("SIGTERM", () => void shutdown());
167
+ await new Promise(() => {});
168
+ } catch (error) {
169
+ const msg = error instanceof Error ? error.message : String(error);
170
+ process.stderr.write(` Error: ${msg}\n`);
171
+ process.exit(1);
172
+ } finally {
173
+ resetConfig();
174
+ resetConfigMeta();
175
+ }
176
+ });
177
+ client.command("doctor").description("Check client environment readiness").action(async () => {
178
+ process.stderr.write("\n First Tree Hub Client Doctor\n\n");
179
+ printResults([
180
+ checkNodeVersion(),
181
+ checkClientConfig(),
182
+ await checkServerReachable(),
183
+ checkAgentConfigs(),
184
+ await checkAgentTokens(),
185
+ await checkWebSocket()
186
+ ]);
187
+ });
188
+ client.command("stop").description("Stop the client (sends SIGTERM to running process)").action(() => {
189
+ process.stderr.write(" Client stop: use Ctrl+C or `kill` the running process.\n");
190
+ process.stderr.write(" Daemon mode with PID file is planned for a future release.\n");
191
+ });
192
+ client.command("status").description("Show client and agent connection status").action(() => {
193
+ const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
194
+ try {
195
+ const agents = loadAgents({
196
+ schema: agentConfigSchema,
197
+ agentsDir
198
+ });
199
+ if (agents.size === 0) {
200
+ process.stderr.write(" No agents configured.\n");
201
+ return;
202
+ }
203
+ process.stderr.write("\n Configured agents:\n\n");
204
+ for (const [name, config] of agents) {
205
+ const masked = config.token.length > 8 ? `${config.token.slice(0, 6)}***${config.token.slice(-2)}` : "***";
206
+ process.stderr.write(` ${name.padEnd(20)} type: ${config.type.padEnd(14)} token: ${masked}\n`);
207
+ }
208
+ process.stderr.write("\n");
209
+ } catch {
210
+ process.stderr.write(" No agents directory found.\n");
211
+ }
212
+ });
213
+ client.command("add [name]").description("Add an agent instance").option("-t, --token <token>", "Agent token").action(async (name, options) => {
214
+ try {
215
+ let agentName = name;
216
+ let agentToken = options?.token;
217
+ if (!agentName || !agentToken) {
218
+ const result = await promptAddAgent();
219
+ agentName = agentName ?? result.name;
220
+ agentToken = agentToken ?? result.token;
221
+ }
222
+ const agentDir = join(DEFAULT_CONFIG_DIR, "agents", agentName);
223
+ mkdirSync(agentDir, {
224
+ recursive: true,
225
+ mode: 448
226
+ });
227
+ setConfigValue(join(agentDir, "agent.yaml"), "token", agentToken);
228
+ process.stderr.write(` Agent "${agentName}" added.\n`);
229
+ process.stderr.write(` Config: ${join(agentDir, "agent.yaml")}\n`);
230
+ } catch (error) {
231
+ if (error.name === "ExitPromptError") {
232
+ process.stderr.write("\n Cancelled.\n");
233
+ return;
234
+ }
235
+ const msg = error instanceof Error ? error.message : String(error);
236
+ process.stderr.write(` Error: ${msg}\n`);
237
+ process.exit(1);
238
+ }
239
+ });
240
+ client.command("remove <name>").description("Remove an agent instance").action((name) => {
241
+ const agentDir = join(DEFAULT_CONFIG_DIR, "agents", name);
242
+ if (!existsSync(agentDir)) {
243
+ process.stderr.write(` Agent "${name}" not found.\n`);
244
+ process.exit(1);
245
+ }
246
+ rmSync(agentDir, {
247
+ recursive: true,
248
+ force: true
249
+ });
250
+ process.stderr.write(` Agent "${name}" removed.\n`);
251
+ });
252
+ client.command("list").description("List configured agents").action(() => {
253
+ const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
254
+ try {
255
+ const agents = loadAgents({
256
+ schema: agentConfigSchema,
257
+ agentsDir
258
+ });
259
+ if (agents.size === 0) {
260
+ process.stderr.write(" No agents configured.\n");
261
+ return;
262
+ }
263
+ for (const [name, config] of agents) {
264
+ const masked = config.token.length > 8 ? `${config.token.slice(0, 6)}***${config.token.slice(-2)}` : "***";
265
+ process.stderr.write(` ${name.padEnd(20)} type: ${config.type.padEnd(14)} token: ${masked}\n`);
266
+ }
267
+ } catch {
268
+ process.stderr.write(" No agents configured.\n");
269
+ }
270
+ });
271
+ }
272
+ //#endregion
273
+ //#region src/commands/config.ts
274
+ function resolveConfigPath(flags) {
275
+ if (flags.agent) return {
276
+ path: join(DEFAULT_CONFIG_DIR, "agents", flags.agent, "agent.yaml"),
277
+ schema: agentConfigSchema
278
+ };
279
+ if (flags.client) return {
280
+ path: join(DEFAULT_CONFIG_DIR, "client.yaml"),
281
+ schema: clientConfigSchema
282
+ };
283
+ return {
284
+ path: join(DEFAULT_CONFIG_DIR, "server.yaml"),
285
+ schema: serverConfigSchema
286
+ };
287
+ }
288
+ function addScopeOptions(cmd) {
289
+ return cmd.option("-s, --server", "Server config scope (default)").option("-c, --client", "Client config scope").option("-a, --agent <name>", "Agent config scope");
290
+ }
291
+ function registerConfigCommands(program) {
292
+ const config = program.command("config").description("Configuration management");
293
+ config.command("setup").description("Interactive configuration wizard").option("-s, --server", "Configure server (default)").option("-c, --client", "Configure client").action(async (flags) => {
294
+ try {
295
+ await promptMissingFields({
296
+ schema: flags.client ? clientConfigSchema : serverConfigSchema,
297
+ role: flags.client ? "client" : "server"
298
+ });
299
+ process.stderr.write("\n Configuration saved.\n");
300
+ } catch (error) {
301
+ if (error.name === "ExitPromptError") {
302
+ process.stderr.write("\n Cancelled.\n");
303
+ return;
304
+ }
305
+ throw error;
306
+ }
307
+ });
308
+ addScopeOptions(config.command("set").description("Set a config value")).argument("<key>", "Config key (dot notation, e.g. database.url)").argument("<value>", "Config value").action((key, value, flags) => {
309
+ const { path } = resolveConfigPath(flags);
310
+ let parsed = value;
311
+ if (value === "true") parsed = true;
312
+ else if (value === "false") parsed = false;
313
+ else if (/^\d+$/.test(value)) parsed = Number(value);
314
+ setConfigValue(path, key, parsed);
315
+ process.stderr.write(` Set ${key} in ${path}\n`);
316
+ });
317
+ addScopeOptions(config.command("get").description("Get a config value")).argument("<key>", "Config key (dot notation)").option("--show-secrets", "Show secret values in plaintext").action((key, flags) => {
318
+ const { path, schema } = resolveConfigPath(flags);
319
+ const value = getConfigValue(path, key);
320
+ if (value === void 0) process.stderr.write(` ${key}: (not set)\n`);
321
+ else {
322
+ const display = isSecretField(schema, key) && !flags.showSecrets ? "***" : String(value);
323
+ process.stderr.write(` ${key}: ${display}\n`);
324
+ }
325
+ });
326
+ addScopeOptions(config.command("list").description("List all config values")).option("--show-secrets", "Show secret values in plaintext").action((flags) => {
327
+ const { path, schema } = resolveConfigPath(flags);
328
+ const values = readConfigFile(path);
329
+ if (Object.keys(values).length === 0) {
330
+ process.stderr.write(` No config found at ${path}\n`);
331
+ return;
332
+ }
333
+ process.stderr.write(`\n Config: ${path}\n\n`);
334
+ printFlat(values, schema, "", flags.showSecrets ?? false);
335
+ process.stderr.write("\n");
336
+ });
337
+ }
338
+ function printFlat(obj, schema, prefix, showSecrets) {
339
+ for (const [key, value] of Object.entries(obj)) {
340
+ const fullKey = prefix ? `${prefix}.${key}` : key;
341
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) printFlat(value, schema, fullKey, showSecrets);
342
+ else {
343
+ const display = isSecretField(schema, fullKey) && !showSecrets ? "***" : String(value);
344
+ process.stderr.write(` ${fullKey.padEnd(30)} ${display}\n`);
345
+ }
346
+ }
347
+ }
348
+ /** Check if a dot-path corresponds to a secret field in the schema. */
349
+ function isSecretField(schema, dotPath) {
350
+ const parts = dotPath.split(".");
351
+ let current = schema;
352
+ for (const part of parts) {
353
+ if (current === null || current === void 0 || typeof current !== "object") return false;
354
+ const obj = current;
355
+ if (obj._tag === "optional") current = obj.shape[part];
356
+ else if (obj._tag === "field") return false;
357
+ else current = obj[part];
358
+ }
359
+ if (typeof current === "object" && current !== null && "_tag" in current) {
360
+ const field = current;
361
+ if (field._tag === "field") return field.options?.secret ?? false;
362
+ }
363
+ return false;
364
+ }
365
+ //#endregion
366
+ //#region src/commands/db.ts
367
+ function registerDbCommands(program) {
368
+ program.command("db").description("Database management").command("migrate").description("Run database migrations").action(async () => {
369
+ try {
370
+ const tableCount = await runMigrations((await initConfig({
371
+ schema: serverConfigSchema,
372
+ role: "server"
373
+ })).database.url);
374
+ process.stderr.write(` Migrations complete (${tableCount} tables)\n`);
375
+ } catch (error) {
376
+ const msg = error instanceof Error ? error.message : String(error);
377
+ process.stderr.write(` Error: ${msg}\n`);
378
+ process.exit(1);
379
+ }
380
+ });
381
+ }
382
+ //#endregion
383
+ //#region src/commands/history.ts
384
+ function registerHistoryCommand(program) {
385
+ 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) => {
386
+ try {
387
+ const limit = parseLimit(options.limit, 100);
388
+ success(await createSdk().listMessages(chatId, {
389
+ limit,
390
+ cursor: options.cursor
391
+ }));
392
+ } catch (error) {
393
+ handleError(error);
394
+ }
395
+ });
396
+ }
397
+ //#endregion
398
+ //#region src/commands/send.ts
399
+ const MAX_STDIN_BYTES = 10 * 1024 * 1024;
400
+ /** Read all of stdin as a string. Returns null if stdin is a TTY. */
401
+ function readStdin() {
402
+ if (process.stdin.isTTY) return Promise.resolve(null);
403
+ return new Promise((resolve, reject) => {
404
+ const chunks = [];
405
+ let totalSize = 0;
406
+ process.stdin.on("data", (chunk) => {
407
+ totalSize += chunk.length;
408
+ if (totalSize > MAX_STDIN_BYTES) {
409
+ process.stdin.destroy();
410
+ reject(/* @__PURE__ */ new Error(`stdin exceeds ${MAX_STDIN_BYTES} bytes`));
411
+ return;
412
+ }
413
+ chunks.push(chunk);
414
+ });
415
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
416
+ process.stdin.on("error", reject);
417
+ });
418
+ }
419
+ function registerSendCommand(program) {
420
+ 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) => {
421
+ try {
422
+ const content = message ?? await readStdin();
423
+ if (!content) fail("NO_MESSAGE", "No message provided. Pass as argument or pipe via stdin.", 2);
424
+ let metadata;
425
+ if (options.metadata) try {
426
+ metadata = JSON.parse(options.metadata);
427
+ } catch {
428
+ fail("INVALID_METADATA", "Metadata must be valid JSON.", 2);
429
+ }
430
+ const sdk = createSdk();
431
+ if (options.chat) success(await sdk.sendMessage(target, {
432
+ format: options.format,
433
+ content,
434
+ metadata,
435
+ inReplyTo: options.replyTo,
436
+ replyToInbox: options.replyToInbox,
437
+ replyToChat: options.replyToChat
438
+ }));
439
+ else success(await sdk.sendToAgent(target, {
440
+ format: options.format,
441
+ content,
442
+ metadata,
443
+ replyToInbox: options.replyToInbox,
444
+ replyToChat: options.replyToChat
445
+ }));
446
+ } catch (error) {
447
+ handleError(error);
448
+ }
449
+ });
450
+ }
451
+ //#endregion
452
+ //#region src/commands/server.ts
453
+ function registerServerCommands(program) {
454
+ const server = program.command("server").description("Manage First Tree Hub server");
455
+ server.command("start").description("Start the server (auto-provisions PostgreSQL if needed)").option("--port <number>", "Server port (default: 8000)", Number.parseInt).option("--host <address>", "Bind address (default: 127.0.0.1)").option("--database-url <url>", "Use an existing PostgreSQL (skip Docker)").option("--no-interactive", "Skip interactive prompts (for Docker/CI)").action(async (options) => {
456
+ try {
457
+ await startServer({
458
+ ...options,
459
+ noInteractive: options.interactive === false
460
+ });
461
+ } catch (error) {
462
+ const msg = error instanceof Error ? error.message : String(error);
463
+ process.stderr.write(`\n Error: ${msg}\n\n`);
464
+ process.exit(1);
465
+ }
466
+ });
467
+ server.command("stop").description("Stop the managed PostgreSQL container").action(() => {
468
+ if (stopPostgres()) process.stderr.write(" PostgreSQL container stopped.\n");
469
+ else process.stderr.write(" No managed PostgreSQL container found.\n");
470
+ });
471
+ server.command("doctor").description("Check server environment readiness").action(async () => {
472
+ process.stderr.write("\n First Tree Hub Server Doctor\n\n");
473
+ printResults([
474
+ checkNodeVersion(),
475
+ checkDocker(),
476
+ checkServerConfig(),
477
+ await checkDatabase(),
478
+ await checkGitHubToken(),
479
+ await checkContextTreeRepo(),
480
+ await checkServerHealth()
481
+ ]);
482
+ });
483
+ server.command("status").description("Show server health and status").action(async () => {
484
+ const url = process.env.FIRST_TREE_HUB_SERVER_URL ?? "http://localhost:8000";
485
+ try {
486
+ const res = await fetch(`${url}/api/v1/health`);
487
+ if (res.ok) {
488
+ const data = await res.json();
489
+ console.log(JSON.stringify(data, null, 2));
490
+ } else {
491
+ process.stderr.write(` Server returned ${res.status}\n`);
492
+ process.exit(1);
493
+ }
494
+ } catch {
495
+ process.stderr.write(` Cannot connect to ${url}\n`);
496
+ process.exit(1);
497
+ }
498
+ });
499
+ }
500
+ //#endregion
501
+ //#region src/commands/status.ts
502
+ function registerStatusCommand(program) {
503
+ program.command("status").description("Global overview — server health + configured agents").action(async () => {
504
+ process.stderr.write("\n");
505
+ const serverConfig = readConfigFile(join(DEFAULT_CONFIG_DIR, "server.yaml"));
506
+ const serverPort = getNestedValue(serverConfig, "server.port") ?? 8e3;
507
+ const serverUrl = `http://${getNestedValue(serverConfig, "server.host") ?? "127.0.0.1"}:${serverPort}`;
508
+ try {
509
+ const res = await fetch(`${serverUrl}/api/v1/health`);
510
+ if (res.ok) {
511
+ const data = await res.json();
512
+ const uptime = data.uptime_seconds ? formatUptime(data.uptime_seconds) : "unknown";
513
+ process.stderr.write(` Server: ✓ running (${serverUrl}, uptime: ${uptime})\n`);
514
+ } else process.stderr.write(` Server: ✗ unhealthy (${res.status})\n`);
515
+ } catch {
516
+ process.stderr.write(` Server: ✗ not running (${serverUrl})\n`);
517
+ }
518
+ const dbProvider = getNestedValue(serverConfig, "database.provider") ?? "unknown";
519
+ const hasDbUrl = getNestedValue(serverConfig, "database.url") !== void 0;
520
+ process.stderr.write(` Database: ${hasDbUrl ? "✓ configured" : "✗ not configured"} (${dbProvider})\n`);
521
+ const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
522
+ if (existsSync(agentsDir)) try {
523
+ const agents = loadAgents({
524
+ schema: agentConfigSchema,
525
+ agentsDir
526
+ });
527
+ process.stderr.write(` Agents: ${agents.size} configured\n`);
528
+ } catch {
529
+ process.stderr.write(" Agents: error reading config\n");
530
+ }
531
+ else process.stderr.write(" Agents: 0 configured\n");
532
+ const clientConfigPath = join(DEFAULT_CONFIG_DIR, "client.yaml");
533
+ if (existsSync(clientConfigPath)) {
534
+ const clientServerUrl = getNestedValue(readConfigFile(clientConfigPath), "server.url");
535
+ process.stderr.write(` Client: configured → ${clientServerUrl}\n`);
536
+ } else process.stderr.write(" Client: not configured\n");
537
+ process.stderr.write("\n");
538
+ });
539
+ }
540
+ function getNestedValue(obj, dotPath) {
541
+ const parts = dotPath.split(".");
542
+ let current = obj;
543
+ for (const part of parts) {
544
+ if (current === null || current === void 0 || typeof current !== "object") return void 0;
545
+ current = current[part];
546
+ }
547
+ return current;
548
+ }
549
+ function formatUptime(seconds) {
550
+ const days = Math.floor(seconds / 86400);
551
+ const hours = Math.floor(seconds % 86400 / 3600);
552
+ const mins = Math.floor(seconds % 3600 / 60);
553
+ if (days > 0) return `${days}d ${hours}h`;
554
+ if (hours > 0) return `${hours}h ${mins}m`;
555
+ return `${mins}m`;
556
+ }
557
+ //#endregion
558
+ //#region src/cli/connect.ts
559
+ function registerConnectCommand(program) {
560
+ 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) => {
561
+ try {
562
+ registerBuiltinHandlers();
563
+ const config = resolveConfig();
564
+ if (options.server) config.serverUrl = options.server;
565
+ const concurrency = Number.parseInt(options.concurrency, 10) || 5;
566
+ const handlerFactory = getHandlerFactory(options.type);
567
+ const slot = new AgentSlot({
568
+ name: "connect",
569
+ serverUrl: config.serverUrl,
570
+ token: config.token,
571
+ type: options.type,
572
+ handlerFactory,
573
+ session: {
574
+ idle_timeout: 300,
575
+ max_sessions: 10
576
+ },
577
+ concurrency
578
+ });
579
+ const agent = await slot.start();
580
+ log("connect", `Registered as ${agent.displayName ?? agent.agentId} (${agent.agentId})`);
581
+ const shutdown = async () => {
582
+ log("connect", "Shutting down...");
583
+ await slot.stop();
584
+ process.exit(0);
585
+ };
586
+ process.on("SIGINT", () => void shutdown());
587
+ process.on("SIGTERM", () => void shutdown());
588
+ } catch (error) {
589
+ handleError(error);
590
+ }
591
+ });
592
+ }
593
+ //#endregion
594
+ //#region src/cli/start.ts
595
+ function registerStartCommand(program) {
596
+ 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) => {
597
+ try {
598
+ registerBuiltinHandlers();
599
+ log("runtime", `Loading config from ${options.config}`);
600
+ const config = loadRuntimeConfig(options.config);
601
+ if (options.server) config.server = options.server;
602
+ const shutdownTimeout = Number.parseInt(options.shutdownTimeout, 10);
603
+ if (Number.isNaN(shutdownTimeout) || shutdownTimeout < 0) fail("INVALID_OPTION", "shutdown-timeout must be a positive number", 2);
604
+ await new AgentRuntime({
605
+ config,
606
+ shutdownTimeout
607
+ }).start();
608
+ } catch (error) {
609
+ if (error instanceof Error) {
610
+ log("runtime", `Fatal: ${error.message}`);
611
+ process.exit(1);
612
+ }
613
+ throw error;
614
+ }
615
+ });
616
+ }
617
+ //#endregion
618
+ //#region src/cli/index.ts
619
+ const { version } = createRequire(import.meta.url)("../../package.json");
620
+ const program = new Command();
621
+ program.name("first-tree-hub").description("First Tree Hub — centralized collaboration platform for agent teams").version(version);
622
+ registerServerCommands(program);
623
+ registerClientCommands(program);
624
+ registerDbCommands(program);
625
+ registerAdminCommands(program);
626
+ registerConfigCommands(program);
627
+ registerStatusCommand(program);
628
+ registerConnectCommand(program);
629
+ registerStartCommand(program);
630
+ registerAgentCommands(program);
631
+ registerSendCommand(program);
632
+ registerChatsCommand(program);
633
+ registerHistoryCommand(program);
634
+ program.parse();
635
+ //#endregion
636
+ export {};