@agent-team-foundation/first-tree-hub 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-twds-ZHy.mjs";
|
|
2
|
-
import { z } from "zod";
|
|
3
2
|
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { dirname, join } from "node:path";
|
|
5
|
-
import { parse, stringify } from "yaml";
|
|
6
4
|
import { randomBytes } from "node:crypto";
|
|
7
|
-
import { homedir } from "node:os";
|
|
8
5
|
import { execSync } from "node:child_process";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { parse, stringify } from "yaml";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
9
|
//#region ../shared/dist/config/index.mjs
|
|
10
10
|
/** Declare a config field with a Zod schema and optional metadata. */
|
|
11
11
|
function field(schema, options) {
|
|
@@ -544,7 +544,7 @@ async function bootstrapToken(serverUrl, agentId, options = {}) {
|
|
|
544
544
|
}
|
|
545
545
|
const data = await res.json();
|
|
546
546
|
if (options.saveTo === "agent" || !options.saveTo) {
|
|
547
|
-
const configDir = join(
|
|
547
|
+
const configDir = join(DEFAULT_CONFIG_DIR, "agents", agentId);
|
|
548
548
|
const configPath = `${configDir}/agent.yaml`;
|
|
549
549
|
mkdirSync(configDir, {
|
|
550
550
|
recursive: true,
|
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 {};
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { a as getGitHubUsername, b as serverConfigSchema, c as DEFAULT_CONFIG_DIR, d as clientConfigSchema, f as collectMissingPrompts, h as loadAgents, m as initConfig, r as checkBootstrapStatus, s as resolveServerUrl, t as bootstrapToken$1, u as agentConfigSchema, x as setConfigValue, y as resolveConfigReadonly } from "./bootstrap-
|
|
2
|
-
import { ZodError, z } from "zod";
|
|
1
|
+
import { a as getGitHubUsername, b as serverConfigSchema, c as DEFAULT_CONFIG_DIR, d as clientConfigSchema, f as collectMissingPrompts, h as loadAgents, m as initConfig, r as checkBootstrapStatus, s as resolveServerUrl, t as bootstrapToken$1, u as agentConfigSchema, x as setConfigValue, y as resolveConfigReadonly } from "./bootstrap-CPdLNPme.mjs";
|
|
3
2
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
import {
|
|
4
|
+
import { EventEmitter } from "node:events";
|
|
5
|
+
import WebSocket from "ws";
|
|
6
6
|
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
7
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
8
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
9
|
+
import { ZodError, z } from "zod";
|
|
10
|
+
import "yaml";
|
|
7
11
|
import { homedir } from "node:os";
|
|
8
12
|
import bcrypt from "bcrypt";
|
|
9
13
|
import { and, desc, eq, gt, inArray, isNotNull, isNull, lt, ne, sql } from "drizzle-orm";
|
|
10
14
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
11
15
|
import postgres from "postgres";
|
|
12
|
-
import { execFileSync, execSync } from "node:child_process";
|
|
13
|
-
import { EventEmitter } from "node:events";
|
|
14
|
-
import WebSocket from "ws";
|
|
15
|
-
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
18
18
|
import { input, password, select } from "@inquirer/prompts";
|
|
@@ -24,40 +24,6 @@ import Fastify from "fastify";
|
|
|
24
24
|
import { bigserial, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique } from "drizzle-orm/pg-core";
|
|
25
25
|
import { SignJWT, jwtVerify } from "jose";
|
|
26
26
|
import { Client, EventDispatcher, LoggerLevel, WSClient } from "@larksuiteoapi/node-sdk";
|
|
27
|
-
//#region src/core/admin.ts
|
|
28
|
-
/**
|
|
29
|
-
* Check if any admin user exists.
|
|
30
|
-
*/
|
|
31
|
-
async function hasAdminUser(databaseUrl) {
|
|
32
|
-
const client = postgres(databaseUrl, { max: 1 });
|
|
33
|
-
try {
|
|
34
|
-
return (await client`SELECT count(*)::int AS count FROM admin_users`)[0].count > 0;
|
|
35
|
-
} finally {
|
|
36
|
-
await client.end();
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Create an admin user. Returns the generated password.
|
|
41
|
-
*/
|
|
42
|
-
async function createAdminUser$1(databaseUrl, username, password) {
|
|
43
|
-
const pw = password ?? randomBytes(12).toString("base64url");
|
|
44
|
-
const hash = await bcrypt.hash(pw, 12);
|
|
45
|
-
const client = postgres(databaseUrl, { max: 1 });
|
|
46
|
-
const db = drizzle(client);
|
|
47
|
-
try {
|
|
48
|
-
await db.execute(sql`
|
|
49
|
-
INSERT INTO admin_users (id, username, password_hash, role)
|
|
50
|
-
VALUES (${randomUUID()}, ${username}, ${hash}, 'super_admin')
|
|
51
|
-
`);
|
|
52
|
-
} finally {
|
|
53
|
-
await client.end();
|
|
54
|
-
}
|
|
55
|
-
return {
|
|
56
|
-
username,
|
|
57
|
-
password: pw
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
//#endregion
|
|
61
27
|
//#region ../client/dist/index.mjs
|
|
62
28
|
const FETCH_TIMEOUT_MS = 15e3;
|
|
63
29
|
var FirstTreeHubSDK = class {
|
|
@@ -496,109 +462,7 @@ defineConfig({
|
|
|
496
462
|
webhookMax: field(z.number().default(60), { env: "FIRST_TREE_HUB_RATE_LIMIT_WEBHOOK_MAX" })
|
|
497
463
|
})
|
|
498
464
|
});
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Sync the shared Context Tree git clone.
|
|
502
|
-
*
|
|
503
|
-
* Clones on first run, pulls on subsequent runs.
|
|
504
|
-
* Returns the clone path on success, null on failure (graceful degradation).
|
|
505
|
-
*/
|
|
506
|
-
async function syncContextTree(serverUrl, token, log) {
|
|
507
|
-
try {
|
|
508
|
-
execFileSync("git", ["--version"], { stdio: "ignore" });
|
|
509
|
-
} catch {
|
|
510
|
-
log("Context Tree sync skipped: git is not installed");
|
|
511
|
-
return null;
|
|
512
|
-
}
|
|
513
|
-
let repo;
|
|
514
|
-
let branch;
|
|
515
|
-
try {
|
|
516
|
-
const config = await new FirstTreeHubSDK({
|
|
517
|
-
serverUrl,
|
|
518
|
-
token
|
|
519
|
-
}).getContextTreeConfig();
|
|
520
|
-
repo = config.repo;
|
|
521
|
-
branch = config.branch;
|
|
522
|
-
} catch (err) {
|
|
523
|
-
log(`Context Tree sync skipped: failed to fetch config from server (${err instanceof Error ? err.message : String(err)})`);
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
526
|
-
try {
|
|
527
|
-
if (existsSync(join(CONTEXT_TREE_DIR$1, ".git"))) {
|
|
528
|
-
if (execFileSync("git", [
|
|
529
|
-
"rev-parse",
|
|
530
|
-
"--abbrev-ref",
|
|
531
|
-
"HEAD"
|
|
532
|
-
], {
|
|
533
|
-
cwd: CONTEXT_TREE_DIR$1,
|
|
534
|
-
encoding: "utf-8",
|
|
535
|
-
timeout: 5e3
|
|
536
|
-
}).trim() !== branch) {
|
|
537
|
-
execFileSync("git", ["checkout", branch], {
|
|
538
|
-
cwd: CONTEXT_TREE_DIR$1,
|
|
539
|
-
stdio: "pipe",
|
|
540
|
-
timeout: 1e4
|
|
541
|
-
});
|
|
542
|
-
log(`Context Tree switched to branch ${branch}`);
|
|
543
|
-
}
|
|
544
|
-
execFileSync("git", ["pull", "--ff-only"], {
|
|
545
|
-
cwd: CONTEXT_TREE_DIR$1,
|
|
546
|
-
stdio: "pipe",
|
|
547
|
-
timeout: 3e4
|
|
548
|
-
});
|
|
549
|
-
log(`Context Tree updated (pull)`);
|
|
550
|
-
} else {
|
|
551
|
-
mkdirSync(CONTEXT_TREE_DIR$1, { recursive: true });
|
|
552
|
-
execFileSync("git", [
|
|
553
|
-
"clone",
|
|
554
|
-
"--branch",
|
|
555
|
-
branch,
|
|
556
|
-
"--single-branch",
|
|
557
|
-
repo,
|
|
558
|
-
CONTEXT_TREE_DIR$1
|
|
559
|
-
], {
|
|
560
|
-
stdio: "pipe",
|
|
561
|
-
timeout: 6e4
|
|
562
|
-
});
|
|
563
|
-
log(`Context Tree cloned from ${repo} (branch: ${branch})`);
|
|
564
|
-
}
|
|
565
|
-
return CONTEXT_TREE_DIR$1;
|
|
566
|
-
} catch (err) {
|
|
567
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
568
|
-
log(`Context Tree sync failed: ${msg}`);
|
|
569
|
-
log("Check that git credentials (SSH key or credential helper) are configured for this repo");
|
|
570
|
-
if ((msg.includes("cannot fast-forward") || msg.includes("not possible to fast-forward") || msg.includes("CONFLICT")) && existsSync(join(CONTEXT_TREE_DIR$1, ".git"))) {
|
|
571
|
-
log("Diverged history detected, attempting fresh clone...");
|
|
572
|
-
try {
|
|
573
|
-
rmSync(CONTEXT_TREE_DIR$1, {
|
|
574
|
-
recursive: true,
|
|
575
|
-
force: true
|
|
576
|
-
});
|
|
577
|
-
mkdirSync(CONTEXT_TREE_DIR$1, { recursive: true });
|
|
578
|
-
execFileSync("git", [
|
|
579
|
-
"clone",
|
|
580
|
-
"--branch",
|
|
581
|
-
branch,
|
|
582
|
-
"--single-branch",
|
|
583
|
-
repo,
|
|
584
|
-
CONTEXT_TREE_DIR$1
|
|
585
|
-
], {
|
|
586
|
-
stdio: "pipe",
|
|
587
|
-
timeout: 6e4
|
|
588
|
-
});
|
|
589
|
-
log("Context Tree re-cloned successfully");
|
|
590
|
-
return CONTEXT_TREE_DIR$1;
|
|
591
|
-
} catch {
|
|
592
|
-
log("Context Tree re-clone also failed, continuing without context");
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
if (existsSync(join(CONTEXT_TREE_DIR$1, ".git"))) {
|
|
596
|
-
log("Using existing Context Tree clone despite sync failure");
|
|
597
|
-
return CONTEXT_TREE_DIR$1;
|
|
598
|
-
}
|
|
599
|
-
return null;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
465
|
+
join(DEFAULT_DATA_DIR, "context-tree");
|
|
602
466
|
/**
|
|
603
467
|
* Bootstrap a workspace with .agent/ directory files.
|
|
604
468
|
*
|
|
@@ -1454,92 +1318,44 @@ const agentSlotConfigSchema = z.object({
|
|
|
1454
1318
|
session: sessionConfigSchema.prefault({}),
|
|
1455
1319
|
concurrency: z.number().int().positive().default(5)
|
|
1456
1320
|
});
|
|
1457
|
-
|
|
1321
|
+
z.object({
|
|
1458
1322
|
server: z.url().default("http://localhost:8000"),
|
|
1459
1323
|
agents: z.record(z.string(), agentSlotConfigSchema).refine((agents) => Object.keys(agents).length > 0, "At least one agent must be defined")
|
|
1460
1324
|
});
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
const result = {};
|
|
1473
|
-
for (const [key, value] of Object.entries(obj)) result[key] = deepExpandEnv(value);
|
|
1474
|
-
return result;
|
|
1325
|
+
//#endregion
|
|
1326
|
+
//#region src/core/admin.ts
|
|
1327
|
+
/**
|
|
1328
|
+
* Check if any admin user exists.
|
|
1329
|
+
*/
|
|
1330
|
+
async function hasAdminUser(databaseUrl) {
|
|
1331
|
+
const client = postgres(databaseUrl, { max: 1 });
|
|
1332
|
+
try {
|
|
1333
|
+
return (await client`SELECT count(*)::int AS count FROM admin_users`)[0].count > 0;
|
|
1334
|
+
} finally {
|
|
1335
|
+
await client.end();
|
|
1475
1336
|
}
|
|
1476
|
-
return obj;
|
|
1477
1337
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
const
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
this.slots.push(new AgentSlot({
|
|
1494
|
-
name,
|
|
1495
|
-
serverUrl: this.config.server,
|
|
1496
|
-
token: agentConfig.token,
|
|
1497
|
-
type: agentConfig.type,
|
|
1498
|
-
handlerFactory,
|
|
1499
|
-
session: agentConfig.session,
|
|
1500
|
-
concurrency: agentConfig.concurrency
|
|
1501
|
-
}));
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
/** Start all agent slots and block until shutdown signal. */
|
|
1505
|
-
async start() {
|
|
1506
|
-
const log = (msg) => process.stderr.write(`[runtime] ${msg}\n`);
|
|
1507
|
-
const firstToken = Object.values(this.config.agents)[0]?.token;
|
|
1508
|
-
let contextTreePath = null;
|
|
1509
|
-
if (firstToken) contextTreePath = await syncContextTree(this.config.server, firstToken, log);
|
|
1510
|
-
if (!contextTreePath) log("WARNING: Context Tree sync failed — agents will start without organizational context");
|
|
1511
|
-
log(`Starting ${this.slots.length} agent(s)...`);
|
|
1512
|
-
const results = await Promise.allSettled(this.slots.map((slot) => slot.start(contextTreePath)));
|
|
1513
|
-
let failed = 0;
|
|
1514
|
-
for (const result of results) if (result.status === "rejected") {
|
|
1515
|
-
log(`Failed to start agent: ${result.reason instanceof Error ? result.reason.message : result.reason}`);
|
|
1516
|
-
failed++;
|
|
1517
|
-
}
|
|
1518
|
-
if (failed === this.slots.length) throw new Error("All agents failed to start");
|
|
1519
|
-
log("Ready. Press Ctrl+C to stop.");
|
|
1520
|
-
await new Promise((resolve) => {
|
|
1521
|
-
const shutdown = async () => {
|
|
1522
|
-
if (this.stopping) return;
|
|
1523
|
-
this.stopping = true;
|
|
1524
|
-
log("Shutting down...");
|
|
1525
|
-
const timer = setTimeout(() => {
|
|
1526
|
-
log("Shutdown timeout reached, forcing exit");
|
|
1527
|
-
process.exit(1);
|
|
1528
|
-
}, this.shutdownTimeout);
|
|
1529
|
-
await this.stop();
|
|
1530
|
-
clearTimeout(timer);
|
|
1531
|
-
log("Stopped");
|
|
1532
|
-
resolve();
|
|
1533
|
-
};
|
|
1534
|
-
process.on("SIGINT", shutdown);
|
|
1535
|
-
process.on("SIGTERM", shutdown);
|
|
1536
|
-
});
|
|
1537
|
-
}
|
|
1538
|
-
/** Stop all slots. */
|
|
1539
|
-
async stop() {
|
|
1540
|
-
await Promise.allSettled(this.slots.map((slot) => slot.stop()));
|
|
1338
|
+
/**
|
|
1339
|
+
* Create an admin user. Returns the generated password.
|
|
1340
|
+
*/
|
|
1341
|
+
async function createAdminUser$1(databaseUrl, username, password) {
|
|
1342
|
+
const pw = password ?? randomBytes(12).toString("base64url");
|
|
1343
|
+
const hash = await bcrypt.hash(pw, 12);
|
|
1344
|
+
const client = postgres(databaseUrl, { max: 1 });
|
|
1345
|
+
const db = drizzle(client);
|
|
1346
|
+
try {
|
|
1347
|
+
await db.execute(sql`
|
|
1348
|
+
INSERT INTO admin_users (id, username, password_hash, role)
|
|
1349
|
+
VALUES (${randomUUID()}, ${username}, ${hash}, 'super_admin')
|
|
1350
|
+
`);
|
|
1351
|
+
} finally {
|
|
1352
|
+
await client.end();
|
|
1541
1353
|
}
|
|
1542
|
-
|
|
1354
|
+
return {
|
|
1355
|
+
username,
|
|
1356
|
+
password: pw
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1543
1359
|
//#endregion
|
|
1544
1360
|
//#region src/core/client-runtime.ts
|
|
1545
1361
|
/**
|
|
@@ -2564,13 +2380,16 @@ async function onboardContinue(args) {
|
|
|
2564
2380
|
process.stderr.write("\n✅ Onboard complete!\n\n");
|
|
2565
2381
|
process.stderr.write(` Human: ${mergedArgs.id}\n`);
|
|
2566
2382
|
if (mergedArgs.assistant) process.stderr.write(` Assistant: ${mergedArgs.assistant}\n`);
|
|
2567
|
-
process.stderr.write(` Token: ~/.first-tree-hub/agents/${agentToBootstrap}/agent.yaml\n`);
|
|
2383
|
+
process.stderr.write(` Token: ~/.first-tree-hub/config/agents/${agentToBootstrap}/agent.yaml\n`);
|
|
2568
2384
|
if (mergedArgs.feishuBotAppId) process.stderr.write(` Feishu: bot bound (${mergedArgs.feishuBotAppId})\n`);
|
|
2385
|
+
setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", serverUrl);
|
|
2569
2386
|
if (mergedArgs.type === "human") {
|
|
2570
2387
|
process.stderr.write("\n Next step — bind your Feishu account:\n");
|
|
2571
2388
|
process.stderr.write(` Send this message to the bot in Feishu: /bind ${mergedArgs.id}\n`);
|
|
2572
2389
|
if (!mergedArgs.feishuBotAppId) process.stderr.write(" (requires a Feishu bot to be configured in the system)\n");
|
|
2573
2390
|
}
|
|
2391
|
+
process.stderr.write("\n Start the agent:\n");
|
|
2392
|
+
process.stderr.write(" first-tree-hub client start\n");
|
|
2574
2393
|
process.stderr.write("\n");
|
|
2575
2394
|
}
|
|
2576
2395
|
function createMemberNodeMd(repoPath, data) {
|
|
@@ -6594,4 +6413,4 @@ function resolveWebDist() {
|
|
|
6594
6413
|
} catch {}
|
|
6595
6414
|
}
|
|
6596
6415
|
//#endregion
|
|
6597
|
-
export { ClientRuntime as A,
|
|
6416
|
+
export { ClientRuntime as A, checkWebSocket as C, ensurePostgres as D, status as E, SessionRegistry as F, cleanWorkspaces as I, hasAdminUser as M, FirstTreeHubSDK as N, isDockerAvailable as O, SdkError as P, checkServerReachable as S, blank as T, checkDocker as _, formatCheckReport as a, checkServerConfig as b, onboardContinue as c, runMigrations as d, checkAgentConfigs as f, checkDatabase as g, checkContextTreeRepo as h, promptMissingFields as i, createAdminUser$1 as j, stopPostgres as k, onboardCreate as l, checkClientConfig as m, isInteractive as n, loadOnboardState as o, checkAgentTokens as p, promptAddAgent as r, onboardCheck as s, startServer as t, saveOnboardState as u, checkGitHubToken as v, printResults as w, checkServerHealth as x, checkNodeVersion as y };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import {
|
|
1
|
+
import { A as ClientRuntime, C as checkWebSocket, D as ensurePostgres, E as status, M as hasAdminUser, N as FirstTreeHubSDK, O as isDockerAvailable, P as SdkError, S as checkServerReachable, T as blank, _ as checkDocker, a as formatCheckReport, b as checkServerConfig, c as onboardContinue, d as runMigrations, f as checkAgentConfigs, g as checkDatabase, h as checkContextTreeRepo, i as promptMissingFields, j as createAdminUser, k as stopPostgres, l as onboardCreate, m as checkClientConfig, n as isInteractive, p as checkAgentTokens, r as promptAddAgent, s as onboardCheck, t as startServer, v as checkGitHubToken, w as printResults, x as checkServerHealth, y as checkNodeVersion } from "./core-CZjUVAU-.mjs";
|
|
2
|
+
import { a as getGitHubUsername, i as getGitHubToken, o as resolveAgentToken, r as checkBootstrapStatus, s as resolveServerUrl, t as bootstrapToken } from "./bootstrap-CPdLNPme.mjs";
|
|
3
3
|
import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-Y4m2zFc3.mjs";
|
|
4
4
|
export { ClientRuntime, FirstTreeHubSDK, SdkError, bindFeishuBot, bindFeishuUser, blank, bootstrapToken, checkAgentConfigs, checkAgentTokens, checkBootstrapStatus, checkClientConfig, checkContextTreeRepo, checkDatabase, checkDocker, checkGitHubToken, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createAdminUser, ensurePostgres, formatCheckReport, getGitHubToken, getGitHubUsername, hasAdminUser, isDockerAvailable, isInteractive, onboardCheck, onboardContinue, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAgentToken, resolveServerUrl, runMigrations, startServer, status, stopPostgres };
|