@agent-team-foundation/first-tree-hub 0.3.2 → 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.
- package/dist/{bootstrap-B9JsJR3Z.mjs → bootstrap-CPdLNPme.mjs} +5 -5
- package/dist/cli/index.mjs +231 -341
- package/dist/{core-vR5jYKHZ.mjs → core-CZjUVAU-.mjs} +57 -238
- package/dist/index.mjs +2 -2
- package/dist/web/assets/index-BHn3RVzY.js +272 -0
- package/dist/web/index.html +1 -1
- package/package.json +2 -2
- package/dist/web/assets/index-B1dQmYGJ.js +0 -234
package/dist/cli/index.mjs
CHANGED
|
@@ -1,30 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { _ as
|
|
3
|
-
import {
|
|
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
|
|
42
|
+
serverUrl,
|
|
54
43
|
token
|
|
55
44
|
};
|
|
56
45
|
}
|
|
57
|
-
function createSdk
|
|
46
|
+
function createSdk() {
|
|
58
47
|
return new FirstTreeHubSDK(resolveAgentConfig());
|
|
59
48
|
}
|
|
60
|
-
function
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
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
|
-
|
|
178
|
+
fail("BOOTSTRAP_ERROR", error instanceof Error ? error.message : String(error));
|
|
86
179
|
}
|
|
87
180
|
});
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
|
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-
|
|
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-
|
|
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
|
-
|
|
666
|
-
|
|
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
|
-
|
|
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 {};
|