@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.
- package/dist/cli/index.mjs +636 -0
- package/dist/core-CD3xEbyB.mjs +27914 -0
- package/dist/drizzle/0000_shocking_darkhawk.sql +92 -0
- package/dist/drizzle/0001_v2_schema_updates.sql +26 -0
- package/dist/drizzle/0002_adapter_tables.sql +64 -0
- package/dist/drizzle/0003_feishu_adapter.sql +21 -0
- package/dist/drizzle/0004_adapter_refactor.sql +13 -0
- package/dist/drizzle/0005_delegate_mention.sql +1 -0
- package/dist/drizzle/meta/0000_snapshot.json +687 -0
- package/dist/drizzle/meta/0001_snapshot.json +687 -0
- package/dist/drizzle/meta/_journal.json +48 -0
- package/dist/index.mjs +2 -0
- package/dist/web/assets/index-CHZINY3I.js +229 -0
- package/dist/web/assets/index-Drt799Rs.css +1 -0
- package/dist/web/favicon.svg +21 -0
- package/dist/web/index.html +14 -0
- package/package.json +64 -0
|
@@ -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 {};
|