@agent-team-foundation/first-tree-hub 0.3.0 → 0.3.1
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 +583 -0
- package/dist/cli/index.mjs +191 -1
- package/dist/{core-uG-Hkr9K.mjs → core-D-c9r7D6.mjs} +854 -488
- package/dist/feishu-Y4m2zFc3.mjs +51 -0
- package/dist/index.mjs +4 -2
- package/dist/rolldown-runtime-twds-ZHy.mjs +14 -0
- package/dist/web/assets/{index-B8NgnD3u.js → index-B1dQmYGJ.js} +23 -23
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
|
@@ -1,13 +1,15 @@
|
|
|
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-B9JsJR3Z.mjs";
|
|
1
2
|
import { ZodError, z } from "zod";
|
|
2
3
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import { parse
|
|
5
|
+
import { parse } from "yaml";
|
|
5
6
|
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
6
7
|
import { homedir } from "node:os";
|
|
7
8
|
import bcrypt from "bcrypt";
|
|
8
9
|
import { and, desc, eq, gt, inArray, isNotNull, isNull, lt, ne, sql } from "drizzle-orm";
|
|
9
10
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
10
11
|
import postgres from "postgres";
|
|
12
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
11
13
|
import { EventEmitter } from "node:events";
|
|
12
14
|
import WebSocket$1 from "ws";
|
|
13
15
|
import { dirname as dirname$1, join as join$1 } from "path";
|
|
@@ -22,7 +24,6 @@ import { cwd } from "process";
|
|
|
22
24
|
import * as r from "fs";
|
|
23
25
|
import { realpathSync } from "fs";
|
|
24
26
|
import { promisify } from "util";
|
|
25
|
-
import { execFileSync, execSync } from "node:child_process";
|
|
26
27
|
import { fileURLToPath as fileURLToPath$1 } from "node:url";
|
|
27
28
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
28
29
|
import { input, password, select } from "@inquirer/prompts";
|
|
@@ -34,463 +35,6 @@ import Fastify from "fastify";
|
|
|
34
35
|
import { bigserial, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique } from "drizzle-orm/pg-core";
|
|
35
36
|
import { SignJWT, jwtVerify } from "jose";
|
|
36
37
|
import { Client, EventDispatcher, LoggerLevel, WSClient } from "@larksuiteoapi/node-sdk";
|
|
37
|
-
//#region ../shared/dist/config/index.mjs
|
|
38
|
-
/** Declare a config field with a Zod schema and optional metadata. */
|
|
39
|
-
function field$1(schema, options) {
|
|
40
|
-
return {
|
|
41
|
-
_tag: "field",
|
|
42
|
-
_type: void 0,
|
|
43
|
-
schema,
|
|
44
|
-
options: options ?? {}
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
/** Mark a config group as optional — present only when at least one field has an explicit value. */
|
|
48
|
-
function optional$1(shape) {
|
|
49
|
-
return {
|
|
50
|
-
_tag: "optional",
|
|
51
|
-
shape
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
/** Define a config shape. Identity function used for type inference. */
|
|
55
|
-
function defineConfig$1(shape) {
|
|
56
|
-
return shape;
|
|
57
|
-
}
|
|
58
|
-
const agentConfigSchema = defineConfig$1({
|
|
59
|
-
token: field$1(z.string(), { secret: true }),
|
|
60
|
-
type: field$1(z.string().default("claude-code")),
|
|
61
|
-
concurrency: field$1(z.number().int().positive().default(5)),
|
|
62
|
-
session: {
|
|
63
|
-
idle_timeout: field$1(z.number().int().positive().default(300)),
|
|
64
|
-
max_sessions: field$1(z.number().int().positive().default(10))
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
/** Store the resolved config as a singleton. Called by initConfig(). */
|
|
68
|
-
function setConfig(config) {}
|
|
69
|
-
/** Reset the config singleton. For testing only. */
|
|
70
|
-
function resetConfig() {}
|
|
71
|
-
const clientConfigSchema = defineConfig$1({
|
|
72
|
-
server: { url: field$1(z.string(), {
|
|
73
|
-
env: "FIRST_TREE_HUB_SERVER_URL",
|
|
74
|
-
prompt: {
|
|
75
|
-
message: "Server URL:",
|
|
76
|
-
default: "http://localhost:8000"
|
|
77
|
-
}
|
|
78
|
-
}) },
|
|
79
|
-
logLevel: field$1(z.enum([
|
|
80
|
-
"debug",
|
|
81
|
-
"info",
|
|
82
|
-
"warn",
|
|
83
|
-
"error"
|
|
84
|
-
]).default("info"), { env: "FIRST_TREE_HUB_LOG_LEVEL" })
|
|
85
|
-
});
|
|
86
|
-
const DEFAULT_HOME_DIR$1 = join(homedir(), ".first-tree-hub");
|
|
87
|
-
const DEFAULT_CONFIG_DIR = join(DEFAULT_HOME_DIR$1, "config");
|
|
88
|
-
const DEFAULT_DATA_DIR$1 = join(DEFAULT_HOME_DIR$1, "data");
|
|
89
|
-
function isFieldDef(value) {
|
|
90
|
-
return typeof value === "object" && value !== null && "_tag" in value && value._tag === "field";
|
|
91
|
-
}
|
|
92
|
-
function isOptionalGroup(value) {
|
|
93
|
-
return typeof value === "object" && value !== null && "_tag" in value && value._tag === "optional";
|
|
94
|
-
}
|
|
95
|
-
function getByPath(obj, path) {
|
|
96
|
-
let current = obj;
|
|
97
|
-
for (const key of path) {
|
|
98
|
-
if (current === null || current === void 0 || typeof current !== "object") return;
|
|
99
|
-
current = current[key];
|
|
100
|
-
}
|
|
101
|
-
return current;
|
|
102
|
-
}
|
|
103
|
-
function setByPath(obj, path, value) {
|
|
104
|
-
let current = obj;
|
|
105
|
-
for (let i = 0; i < path.length - 1; i++) {
|
|
106
|
-
const key = path[i];
|
|
107
|
-
if (key === void 0) continue;
|
|
108
|
-
if (!(key in current) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
|
|
109
|
-
current = current[key];
|
|
110
|
-
}
|
|
111
|
-
const lastKey = path.at(-1);
|
|
112
|
-
if (lastKey !== void 0) current[lastKey] = value;
|
|
113
|
-
}
|
|
114
|
-
/** Unwrap ZodDefault / ZodOptional to get the inner type for coercion. */
|
|
115
|
-
function unwrapZodType(schema) {
|
|
116
|
-
if (schema instanceof z.ZodDefault) return unwrapZodType(schema._def.innerType);
|
|
117
|
-
if (schema instanceof z.ZodOptional) return unwrapZodType(schema._def.innerType);
|
|
118
|
-
return schema;
|
|
119
|
-
}
|
|
120
|
-
/** Coerce a string env var to the JS type expected by the Zod schema. */
|
|
121
|
-
function coerceEnvValue(value, schema) {
|
|
122
|
-
const inner = unwrapZodType(schema);
|
|
123
|
-
if (inner instanceof z.ZodNumber) {
|
|
124
|
-
const num = Number(value);
|
|
125
|
-
return Number.isNaN(num) ? value : num;
|
|
126
|
-
}
|
|
127
|
-
if (inner instanceof z.ZodBoolean) {
|
|
128
|
-
if (value === "true" || value === "1") return true;
|
|
129
|
-
if (value === "false" || value === "0") return false;
|
|
130
|
-
return value;
|
|
131
|
-
}
|
|
132
|
-
return value;
|
|
133
|
-
}
|
|
134
|
-
function builtinAutoGenerate(strategy) {
|
|
135
|
-
const match = /^random:(\w+):(\d+)$/.exec(strategy);
|
|
136
|
-
if (!match) throw new Error(`Unknown auto-generation strategy: ${strategy}`);
|
|
137
|
-
const encoding = match[1];
|
|
138
|
-
const bytes = Number(match[2]);
|
|
139
|
-
if (!encoding) throw new Error(`Invalid auto-generation strategy: ${strategy}`);
|
|
140
|
-
if (encoding === "base64url") return randomBytes(bytes).toString("base64url");
|
|
141
|
-
if (encoding === "hex") return randomBytes(bytes).toString("hex");
|
|
142
|
-
throw new Error(`Unknown random encoding: ${encoding}`);
|
|
143
|
-
}
|
|
144
|
-
function ensureDir(dir) {
|
|
145
|
-
if (!existsSync(dir)) mkdirSync(dir, {
|
|
146
|
-
recursive: true,
|
|
147
|
-
mode: 448
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
function deepMerge(target, source) {
|
|
151
|
-
const result = { ...target };
|
|
152
|
-
for (const [key, value] of Object.entries(source)) if (typeof value === "object" && value !== null && !Array.isArray(value) && typeof result[key] === "object" && result[key] !== null && !Array.isArray(result[key])) result[key] = deepMerge(result[key], value);
|
|
153
|
-
else result[key] = value;
|
|
154
|
-
return result;
|
|
155
|
-
}
|
|
156
|
-
function deepFreeze(obj) {
|
|
157
|
-
if (typeof obj !== "object" || obj === null) return obj;
|
|
158
|
-
Object.freeze(obj);
|
|
159
|
-
for (const value of Object.values(obj)) deepFreeze(value);
|
|
160
|
-
return obj;
|
|
161
|
-
}
|
|
162
|
-
function collectFields(shape, path = [], optionalGroupPath = null) {
|
|
163
|
-
const fields = [];
|
|
164
|
-
for (const [key, value] of Object.entries(shape)) {
|
|
165
|
-
const currentPath = [...path, key];
|
|
166
|
-
if (isFieldDef(value)) fields.push({
|
|
167
|
-
path: currentPath,
|
|
168
|
-
fieldDef: value,
|
|
169
|
-
optionalGroupPath
|
|
170
|
-
});
|
|
171
|
-
else if (isOptionalGroup(value)) fields.push(...collectFields(value.shape, currentPath, currentPath));
|
|
172
|
-
else if (typeof value === "object" && value !== null) fields.push(...collectFields(value, currentPath, optionalGroupPath));
|
|
173
|
-
}
|
|
174
|
-
return fields;
|
|
175
|
-
}
|
|
176
|
-
/** Build a Zod object schema from the config shape for validation. */
|
|
177
|
-
function buildZodSchema(shape) {
|
|
178
|
-
const zodShape = {};
|
|
179
|
-
for (const [key, value] of Object.entries(shape)) if (isFieldDef(value)) zodShape[key] = value.schema;
|
|
180
|
-
else if (isOptionalGroup(value)) zodShape[key] = buildZodSchema(value.shape).optional();
|
|
181
|
-
else if (typeof value === "object" && value !== null) zodShape[key] = buildZodSchema(value).default({});
|
|
182
|
-
return z.object(zodShape);
|
|
183
|
-
}
|
|
184
|
-
function resetConfigMeta() {}
|
|
185
|
-
const CONFIG_HEADER = "# Generated by first-tree-hub. Edit as needed.\n# https://github.com/agent-team-foundation/first-tree-hub\n\n";
|
|
186
|
-
/**
|
|
187
|
-
* Initialize config from the priority chain:
|
|
188
|
-
* CLI args > env vars > YAML file > auto-generated > Zod defaults
|
|
189
|
-
*
|
|
190
|
-
* Auto-generated values are written back to the YAML file.
|
|
191
|
-
* Result is frozen and stored as a singleton accessible via getConfig().
|
|
192
|
-
*/
|
|
193
|
-
async function initConfig(options) {
|
|
194
|
-
const { schema, role, cliArgs = {}, autoGenerators = {} } = options;
|
|
195
|
-
const configPath = join(options.configDir ?? DEFAULT_CONFIG_DIR, `${role}.yaml`);
|
|
196
|
-
let fileValues = {};
|
|
197
|
-
if (existsSync(configPath)) {
|
|
198
|
-
const raw = parse(readFileSync(configPath, "utf-8"));
|
|
199
|
-
if (typeof raw === "object" && raw !== null) fileValues = raw;
|
|
200
|
-
}
|
|
201
|
-
const fields = collectFields(schema);
|
|
202
|
-
const resolved = {};
|
|
203
|
-
const meta = /* @__PURE__ */ new Map();
|
|
204
|
-
const autoGenerated = {};
|
|
205
|
-
const activeOptionalGroups = /* @__PURE__ */ new Set();
|
|
206
|
-
for (const { path, fieldDef, optionalGroupPath } of fields) {
|
|
207
|
-
if (!optionalGroupPath) continue;
|
|
208
|
-
const groupKey = optionalGroupPath.join(".");
|
|
209
|
-
if (activeOptionalGroups.has(groupKey)) continue;
|
|
210
|
-
if (getByPath(cliArgs, path) !== void 0) {
|
|
211
|
-
activeOptionalGroups.add(groupKey);
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
if (fieldDef.options.env) {
|
|
215
|
-
const envValue = process.env[fieldDef.options.env];
|
|
216
|
-
if (envValue !== void 0 && envValue !== "") {
|
|
217
|
-
activeOptionalGroups.add(groupKey);
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (getByPath(fileValues, path) !== void 0) activeOptionalGroups.add(groupKey);
|
|
222
|
-
}
|
|
223
|
-
for (const { path, fieldDef, optionalGroupPath } of fields) {
|
|
224
|
-
const dotPath = path.join(".");
|
|
225
|
-
if (optionalGroupPath && !activeOptionalGroups.has(optionalGroupPath.join("."))) continue;
|
|
226
|
-
const cliValue = getByPath(cliArgs, path);
|
|
227
|
-
if (cliValue !== void 0) {
|
|
228
|
-
setByPath(resolved, path, cliValue);
|
|
229
|
-
meta.set(dotPath, {
|
|
230
|
-
value: cliValue,
|
|
231
|
-
source: "cli",
|
|
232
|
-
secret: fieldDef.options.secret ?? false
|
|
233
|
-
});
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
if (fieldDef.options.env) {
|
|
237
|
-
const envValue = process.env[fieldDef.options.env];
|
|
238
|
-
if (envValue !== void 0 && envValue !== "") {
|
|
239
|
-
const coerced = coerceEnvValue(envValue, fieldDef.schema);
|
|
240
|
-
setByPath(resolved, path, coerced);
|
|
241
|
-
meta.set(dotPath, {
|
|
242
|
-
value: coerced,
|
|
243
|
-
source: "env",
|
|
244
|
-
secret: fieldDef.options.secret ?? false
|
|
245
|
-
});
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
const fileValue = getByPath(fileValues, path);
|
|
250
|
-
if (fileValue !== void 0) {
|
|
251
|
-
setByPath(resolved, path, fileValue);
|
|
252
|
-
meta.set(dotPath, {
|
|
253
|
-
value: fileValue,
|
|
254
|
-
source: "file",
|
|
255
|
-
secret: fieldDef.options.secret ?? false
|
|
256
|
-
});
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
if (fieldDef.options.auto) {
|
|
260
|
-
const strategy = fieldDef.options.auto;
|
|
261
|
-
const customGen = autoGenerators[strategy];
|
|
262
|
-
let generated;
|
|
263
|
-
if (customGen) generated = await customGen();
|
|
264
|
-
else generated = builtinAutoGenerate(strategy);
|
|
265
|
-
setByPath(resolved, path, generated);
|
|
266
|
-
setByPath(autoGenerated, path, generated);
|
|
267
|
-
meta.set(dotPath, {
|
|
268
|
-
value: generated,
|
|
269
|
-
source: "auto",
|
|
270
|
-
secret: fieldDef.options.secret ?? false
|
|
271
|
-
});
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
meta.set(dotPath, {
|
|
275
|
-
value: void 0,
|
|
276
|
-
source: "default",
|
|
277
|
-
secret: fieldDef.options.secret ?? false
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
const result = buildZodSchema(schema).safeParse(resolved);
|
|
281
|
-
if (!result.success) {
|
|
282
|
-
const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
283
|
-
throw new Error(`Configuration validation failed:\n${issues}`);
|
|
284
|
-
}
|
|
285
|
-
const config = result.data;
|
|
286
|
-
for (const { path, fieldDef } of fields) {
|
|
287
|
-
const dotPath = path.join(".");
|
|
288
|
-
if (meta.get(dotPath)?.value === void 0) {
|
|
289
|
-
const val = getByPath(config, path);
|
|
290
|
-
if (val !== void 0) meta.set(dotPath, {
|
|
291
|
-
value: val,
|
|
292
|
-
source: "default",
|
|
293
|
-
secret: fieldDef.options.secret ?? false
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
if (Object.keys(autoGenerated).length > 0) {
|
|
298
|
-
const merged = deepMerge(fileValues, autoGenerated);
|
|
299
|
-
ensureDir(dirname(configPath));
|
|
300
|
-
writeFileSync(configPath, CONFIG_HEADER + stringify(merged), { mode: 384 });
|
|
301
|
-
}
|
|
302
|
-
const frozen = deepFreeze(config);
|
|
303
|
-
setConfig(frozen);
|
|
304
|
-
return frozen;
|
|
305
|
-
}
|
|
306
|
-
/** Set a value in a YAML config file by dot-path. */
|
|
307
|
-
function setConfigValue(configPath, dotPath, value) {
|
|
308
|
-
let fileValues = {};
|
|
309
|
-
if (existsSync(configPath)) {
|
|
310
|
-
const raw = parse(readFileSync(configPath, "utf-8"));
|
|
311
|
-
if (typeof raw === "object" && raw !== null) fileValues = raw;
|
|
312
|
-
}
|
|
313
|
-
setByPath(fileValues, dotPath.split("."), value);
|
|
314
|
-
ensureDir(dirname(configPath));
|
|
315
|
-
writeFileSync(configPath, CONFIG_HEADER + stringify(fileValues), { mode: 384 });
|
|
316
|
-
}
|
|
317
|
-
/** Get a value from a YAML config file by dot-path. */
|
|
318
|
-
function getConfigValue(configPath, dotPath) {
|
|
319
|
-
if (!existsSync(configPath)) return void 0;
|
|
320
|
-
const raw = parse(readFileSync(configPath, "utf-8"));
|
|
321
|
-
if (typeof raw !== "object" || raw === null) return void 0;
|
|
322
|
-
return getByPath(raw, dotPath.split("."));
|
|
323
|
-
}
|
|
324
|
-
/** Read all values from a YAML config file. */
|
|
325
|
-
function readConfigFile(configPath) {
|
|
326
|
-
if (!existsSync(configPath)) return {};
|
|
327
|
-
const raw = parse(readFileSync(configPath, "utf-8"));
|
|
328
|
-
if (typeof raw !== "object" || raw === null) return {};
|
|
329
|
-
return raw;
|
|
330
|
-
}
|
|
331
|
-
/**
|
|
332
|
-
* Scan a config schema and return fields that:
|
|
333
|
-
* 1. Have a `prompt` definition
|
|
334
|
-
* 2. Don't have a value from CLI args, env vars, or the config file
|
|
335
|
-
* 3. Don't have an `auto` strategy (auto-gen fields don't need prompting)
|
|
336
|
-
*
|
|
337
|
-
* Used by CLI to show interactive prompts before calling initConfig().
|
|
338
|
-
*/
|
|
339
|
-
function collectMissingPrompts(options) {
|
|
340
|
-
const { schema, role, cliArgs = {} } = options;
|
|
341
|
-
const configPath = join(options.configDir ?? DEFAULT_CONFIG_DIR, `${role}.yaml`);
|
|
342
|
-
let fileValues = {};
|
|
343
|
-
if (existsSync(configPath)) {
|
|
344
|
-
const raw = parse(readFileSync(configPath, "utf-8"));
|
|
345
|
-
if (typeof raw === "object" && raw !== null) fileValues = raw;
|
|
346
|
-
}
|
|
347
|
-
const fields = collectFields(schema);
|
|
348
|
-
const missing = [];
|
|
349
|
-
for (const { path, fieldDef, optionalGroupPath } of fields) {
|
|
350
|
-
if (optionalGroupPath) continue;
|
|
351
|
-
if (!fieldDef.options.prompt) continue;
|
|
352
|
-
if (getByPath(cliArgs, path) !== void 0) continue;
|
|
353
|
-
if (fieldDef.options.env) {
|
|
354
|
-
const envValue = process.env[fieldDef.options.env];
|
|
355
|
-
if (envValue !== void 0 && envValue !== "") continue;
|
|
356
|
-
}
|
|
357
|
-
if (getByPath(fileValues, path) !== void 0) continue;
|
|
358
|
-
missing.push({
|
|
359
|
-
dotPath: path.join("."),
|
|
360
|
-
prompt: fieldDef.options.prompt
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
return missing;
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* Resolve config values through the same priority chain as initConfig()
|
|
367
|
-
* (env vars > YAML file > Zod defaults), but **without side effects**:
|
|
368
|
-
* - No auto-generation
|
|
369
|
-
* - No file writes
|
|
370
|
-
* - No singleton mutation
|
|
371
|
-
* - Partial results (unresolvable fields are omitted, no validation error)
|
|
372
|
-
*
|
|
373
|
-
* Returns the best-effort resolved config as a plain object.
|
|
374
|
-
*/
|
|
375
|
-
function resolveConfigReadonly(options) {
|
|
376
|
-
const { schema, role } = options;
|
|
377
|
-
const configPath = join(options.configDir ?? DEFAULT_CONFIG_DIR, `${role}.yaml`);
|
|
378
|
-
let fileValues = {};
|
|
379
|
-
if (existsSync(configPath)) {
|
|
380
|
-
const raw = parse(readFileSync(configPath, "utf-8"));
|
|
381
|
-
if (typeof raw === "object" && raw !== null) fileValues = raw;
|
|
382
|
-
}
|
|
383
|
-
const fields = collectFields(schema);
|
|
384
|
-
const resolved = {};
|
|
385
|
-
for (const { path, fieldDef } of fields) {
|
|
386
|
-
if (fieldDef.options.env) {
|
|
387
|
-
const envValue = process.env[fieldDef.options.env];
|
|
388
|
-
if (envValue !== void 0 && envValue !== "") {
|
|
389
|
-
setByPath(resolved, path, coerceEnvValue(envValue, fieldDef.schema));
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
const fileValue = getByPath(fileValues, path);
|
|
394
|
-
if (fileValue !== void 0) {
|
|
395
|
-
setByPath(resolved, path, fileValue);
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
const defaultResult = fieldDef.schema.safeParse(void 0);
|
|
399
|
-
if (defaultResult.success && defaultResult.data !== void 0) setByPath(resolved, path, defaultResult.data);
|
|
400
|
-
}
|
|
401
|
-
return resolved;
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Scan an agents directory and load each agent's config.
|
|
405
|
-
*
|
|
406
|
-
* Expected structure:
|
|
407
|
-
* {agentsDir}/
|
|
408
|
-
* code-reviewer/agent.yaml
|
|
409
|
-
* scheduler/agent.yaml
|
|
410
|
-
*
|
|
411
|
-
* Returns a Map keyed by directory name (agent name).
|
|
412
|
-
*/
|
|
413
|
-
function loadAgents(options) {
|
|
414
|
-
const { schema, agentsDir } = options;
|
|
415
|
-
const result = /* @__PURE__ */ new Map();
|
|
416
|
-
if (!existsSync(agentsDir)) return result;
|
|
417
|
-
const zodSchema = buildZodSchema(schema);
|
|
418
|
-
for (const entry of readdirSync(agentsDir)) {
|
|
419
|
-
const agentDir = join(agentsDir, entry);
|
|
420
|
-
if (!statSync(agentDir).isDirectory()) continue;
|
|
421
|
-
const configPath = join(agentDir, "agent.yaml");
|
|
422
|
-
if (!existsSync(configPath)) continue;
|
|
423
|
-
const raw = parse(readFileSync(configPath, "utf-8"));
|
|
424
|
-
const parsed = zodSchema.parse(raw);
|
|
425
|
-
result.set(entry, parsed);
|
|
426
|
-
}
|
|
427
|
-
return result;
|
|
428
|
-
}
|
|
429
|
-
const serverConfigSchema = defineConfig$1({
|
|
430
|
-
database: {
|
|
431
|
-
url: field$1(z.string(), {
|
|
432
|
-
env: "FIRST_TREE_HUB_DATABASE_URL",
|
|
433
|
-
auto: "docker-pg",
|
|
434
|
-
prompt: {
|
|
435
|
-
message: "PostgreSQL:",
|
|
436
|
-
type: "select",
|
|
437
|
-
choices: [{
|
|
438
|
-
name: "Auto-provision via Docker",
|
|
439
|
-
value: "__auto__"
|
|
440
|
-
}, {
|
|
441
|
-
name: "Provide connection URL",
|
|
442
|
-
value: "__input__"
|
|
443
|
-
}]
|
|
444
|
-
}
|
|
445
|
-
}),
|
|
446
|
-
provider: field$1(z.enum(["docker", "external"]).default("docker"))
|
|
447
|
-
},
|
|
448
|
-
server: {
|
|
449
|
-
port: field$1(z.number().default(8e3), { env: "FIRST_TREE_HUB_PORT" }),
|
|
450
|
-
host: field$1(z.string().default("127.0.0.1"), { env: "FIRST_TREE_HUB_HOST" })
|
|
451
|
-
},
|
|
452
|
-
secrets: {
|
|
453
|
-
jwtSecret: field$1(z.string(), {
|
|
454
|
-
env: "FIRST_TREE_HUB_JWT_SECRET",
|
|
455
|
-
auto: "random:base64url:32",
|
|
456
|
-
secret: true
|
|
457
|
-
}),
|
|
458
|
-
encryptionKey: field$1(z.string(), {
|
|
459
|
-
env: "FIRST_TREE_HUB_ENCRYPTION_KEY",
|
|
460
|
-
auto: "random:hex:32",
|
|
461
|
-
secret: true
|
|
462
|
-
})
|
|
463
|
-
},
|
|
464
|
-
contextTree: {
|
|
465
|
-
repo: field$1(z.string(), {
|
|
466
|
-
env: "FIRST_TREE_HUB_CONTEXT_TREE_REPO",
|
|
467
|
-
prompt: { message: "Context Tree repo URL (e.g. https://github.com/org/first-tree):" }
|
|
468
|
-
}),
|
|
469
|
-
branch: field$1(z.string().default("main")),
|
|
470
|
-
syncInterval: field$1(z.number().default(60))
|
|
471
|
-
},
|
|
472
|
-
github: {
|
|
473
|
-
token: field$1(z.string(), {
|
|
474
|
-
env: "FIRST_TREE_HUB_GITHUB_TOKEN",
|
|
475
|
-
secret: true,
|
|
476
|
-
prompt: {
|
|
477
|
-
message: "GitHub token (create at https://github.com/settings/tokens → repo scope):",
|
|
478
|
-
type: "password"
|
|
479
|
-
}
|
|
480
|
-
}),
|
|
481
|
-
webhookSecret: field$1(z.string().optional(), {
|
|
482
|
-
env: "FIRST_TREE_HUB_GITHUB_WEBHOOK_SECRET",
|
|
483
|
-
secret: true
|
|
484
|
-
})
|
|
485
|
-
},
|
|
486
|
-
cors: optional$1({ origin: field$1(z.string(), { env: "FIRST_TREE_HUB_CORS_ORIGIN" }) }),
|
|
487
|
-
rateLimit: optional$1({
|
|
488
|
-
max: field$1(z.number().default(100), { env: "FIRST_TREE_HUB_RATE_LIMIT_MAX" }),
|
|
489
|
-
loginMax: field$1(z.number().default(5), { env: "FIRST_TREE_HUB_RATE_LIMIT_LOGIN_MAX" }),
|
|
490
|
-
webhookMax: field$1(z.number().default(60), { env: "FIRST_TREE_HUB_RATE_LIMIT_WEBHOOK_MAX" })
|
|
491
|
-
})
|
|
492
|
-
});
|
|
493
|
-
//#endregion
|
|
494
38
|
//#region src/core/admin.ts
|
|
495
39
|
/**
|
|
496
40
|
* Check if any admin user exists.
|
|
@@ -23284,7 +22828,7 @@ defineConfig({
|
|
|
23284
22828
|
webhookMax: field(z.number().default(60), { env: "FIRST_TREE_HUB_RATE_LIMIT_WEBHOOK_MAX" })
|
|
23285
22829
|
})
|
|
23286
22830
|
});
|
|
23287
|
-
const CONTEXT_TREE_DIR = join(DEFAULT_DATA_DIR, "context-tree");
|
|
22831
|
+
const CONTEXT_TREE_DIR$1 = join(DEFAULT_DATA_DIR, "context-tree");
|
|
23288
22832
|
/**
|
|
23289
22833
|
* Sync the shared Context Tree git clone.
|
|
23290
22834
|
*
|
|
@@ -23312,77 +22856,77 @@ async function syncContextTree(serverUrl, token, log) {
|
|
|
23312
22856
|
return null;
|
|
23313
22857
|
}
|
|
23314
22858
|
try {
|
|
23315
|
-
if (existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
|
|
22859
|
+
if (existsSync(join(CONTEXT_TREE_DIR$1, ".git"))) {
|
|
23316
22860
|
if (execFileSync("git", [
|
|
23317
22861
|
"rev-parse",
|
|
23318
22862
|
"--abbrev-ref",
|
|
23319
22863
|
"HEAD"
|
|
23320
22864
|
], {
|
|
23321
|
-
cwd: CONTEXT_TREE_DIR,
|
|
22865
|
+
cwd: CONTEXT_TREE_DIR$1,
|
|
23322
22866
|
encoding: "utf-8",
|
|
23323
22867
|
timeout: 5e3
|
|
23324
22868
|
}).trim() !== branch) {
|
|
23325
22869
|
execFileSync("git", ["checkout", branch], {
|
|
23326
|
-
cwd: CONTEXT_TREE_DIR,
|
|
22870
|
+
cwd: CONTEXT_TREE_DIR$1,
|
|
23327
22871
|
stdio: "pipe",
|
|
23328
22872
|
timeout: 1e4
|
|
23329
22873
|
});
|
|
23330
22874
|
log(`Context Tree switched to branch ${branch}`);
|
|
23331
22875
|
}
|
|
23332
22876
|
execFileSync("git", ["pull", "--ff-only"], {
|
|
23333
|
-
cwd: CONTEXT_TREE_DIR,
|
|
22877
|
+
cwd: CONTEXT_TREE_DIR$1,
|
|
23334
22878
|
stdio: "pipe",
|
|
23335
22879
|
timeout: 3e4
|
|
23336
22880
|
});
|
|
23337
22881
|
log(`Context Tree updated (pull)`);
|
|
23338
22882
|
} else {
|
|
23339
|
-
mkdirSync(CONTEXT_TREE_DIR, { recursive: true });
|
|
22883
|
+
mkdirSync(CONTEXT_TREE_DIR$1, { recursive: true });
|
|
23340
22884
|
execFileSync("git", [
|
|
23341
22885
|
"clone",
|
|
23342
22886
|
"--branch",
|
|
23343
22887
|
branch,
|
|
23344
22888
|
"--single-branch",
|
|
23345
22889
|
repo,
|
|
23346
|
-
CONTEXT_TREE_DIR
|
|
22890
|
+
CONTEXT_TREE_DIR$1
|
|
23347
22891
|
], {
|
|
23348
22892
|
stdio: "pipe",
|
|
23349
22893
|
timeout: 6e4
|
|
23350
22894
|
});
|
|
23351
22895
|
log(`Context Tree cloned from ${repo} (branch: ${branch})`);
|
|
23352
22896
|
}
|
|
23353
|
-
return CONTEXT_TREE_DIR;
|
|
22897
|
+
return CONTEXT_TREE_DIR$1;
|
|
23354
22898
|
} catch (err) {
|
|
23355
22899
|
const msg = err instanceof Error ? err.message : String(err);
|
|
23356
22900
|
log(`Context Tree sync failed: ${msg}`);
|
|
23357
22901
|
log("Check that git credentials (SSH key or credential helper) are configured for this repo");
|
|
23358
|
-
if ((msg.includes("cannot fast-forward") || msg.includes("not possible to fast-forward") || msg.includes("CONFLICT")) && existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
|
|
22902
|
+
if ((msg.includes("cannot fast-forward") || msg.includes("not possible to fast-forward") || msg.includes("CONFLICT")) && existsSync(join(CONTEXT_TREE_DIR$1, ".git"))) {
|
|
23359
22903
|
log("Diverged history detected, attempting fresh clone...");
|
|
23360
22904
|
try {
|
|
23361
|
-
rmSync(CONTEXT_TREE_DIR, {
|
|
22905
|
+
rmSync(CONTEXT_TREE_DIR$1, {
|
|
23362
22906
|
recursive: true,
|
|
23363
22907
|
force: true
|
|
23364
22908
|
});
|
|
23365
|
-
mkdirSync(CONTEXT_TREE_DIR, { recursive: true });
|
|
22909
|
+
mkdirSync(CONTEXT_TREE_DIR$1, { recursive: true });
|
|
23366
22910
|
execFileSync("git", [
|
|
23367
22911
|
"clone",
|
|
23368
22912
|
"--branch",
|
|
23369
22913
|
branch,
|
|
23370
22914
|
"--single-branch",
|
|
23371
22915
|
repo,
|
|
23372
|
-
CONTEXT_TREE_DIR
|
|
22916
|
+
CONTEXT_TREE_DIR$1
|
|
23373
22917
|
], {
|
|
23374
22918
|
stdio: "pipe",
|
|
23375
22919
|
timeout: 6e4
|
|
23376
22920
|
});
|
|
23377
22921
|
log("Context Tree re-cloned successfully");
|
|
23378
|
-
return CONTEXT_TREE_DIR;
|
|
22922
|
+
return CONTEXT_TREE_DIR$1;
|
|
23379
22923
|
} catch {
|
|
23380
22924
|
log("Context Tree re-clone also failed, continuing without context");
|
|
23381
22925
|
}
|
|
23382
22926
|
}
|
|
23383
|
-
if (existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
|
|
22927
|
+
if (existsSync(join(CONTEXT_TREE_DIR$1, ".git"))) {
|
|
23384
22928
|
log("Using existing Context Tree clone despite sync failure");
|
|
23385
|
-
return CONTEXT_TREE_DIR;
|
|
22929
|
+
return CONTEXT_TREE_DIR$1;
|
|
23386
22930
|
}
|
|
23387
22931
|
return null;
|
|
23388
22932
|
}
|
|
@@ -24974,6 +24518,517 @@ async function runMigrations(databaseUrl) {
|
|
|
24974
24518
|
}
|
|
24975
24519
|
}
|
|
24976
24520
|
//#endregion
|
|
24521
|
+
//#region src/core/onboard.ts
|
|
24522
|
+
const STATE_FILE = join(homedir(), ".first-tree-hub", ".onboard-state.json");
|
|
24523
|
+
/** Save current onboard args to state file for resume. */
|
|
24524
|
+
function saveOnboardState(args) {
|
|
24525
|
+
mkdirSync(join(homedir(), ".first-tree-hub"), { recursive: true });
|
|
24526
|
+
writeFileSync(STATE_FILE, JSON.stringify({ args }, null, 2));
|
|
24527
|
+
}
|
|
24528
|
+
/** Load saved onboard args from state file. */
|
|
24529
|
+
function loadOnboardState() {
|
|
24530
|
+
try {
|
|
24531
|
+
return JSON.parse(readFileSync(STATE_FILE, "utf-8")).args;
|
|
24532
|
+
} catch {
|
|
24533
|
+
return null;
|
|
24534
|
+
}
|
|
24535
|
+
}
|
|
24536
|
+
async function onboardCheck(args) {
|
|
24537
|
+
const items = [];
|
|
24538
|
+
let ghUsername = null;
|
|
24539
|
+
try {
|
|
24540
|
+
ghUsername = getGitHubUsername();
|
|
24541
|
+
items.push({
|
|
24542
|
+
key: "github_cli",
|
|
24543
|
+
label: "GitHub CLI",
|
|
24544
|
+
status: "ok",
|
|
24545
|
+
value: `authenticated as ${ghUsername}`
|
|
24546
|
+
});
|
|
24547
|
+
} catch {
|
|
24548
|
+
items.push({
|
|
24549
|
+
key: "github_cli",
|
|
24550
|
+
label: "GitHub CLI",
|
|
24551
|
+
status: "missing_required",
|
|
24552
|
+
hint: "Install and authenticate: gh auth login"
|
|
24553
|
+
});
|
|
24554
|
+
}
|
|
24555
|
+
try {
|
|
24556
|
+
const serverUrl = resolveServerUrl(args.server);
|
|
24557
|
+
items.push({
|
|
24558
|
+
key: "server",
|
|
24559
|
+
label: "Server URL",
|
|
24560
|
+
status: "ok",
|
|
24561
|
+
value: serverUrl
|
|
24562
|
+
});
|
|
24563
|
+
try {
|
|
24564
|
+
const res = await fetch(`${serverUrl}/api/v1/health`);
|
|
24565
|
+
items.push({
|
|
24566
|
+
key: "server_reachable",
|
|
24567
|
+
label: "Server reachable",
|
|
24568
|
+
status: res.ok ? "ok" : "error",
|
|
24569
|
+
value: res.ok ? "yes" : `HTTP ${res.status}`
|
|
24570
|
+
});
|
|
24571
|
+
} catch {
|
|
24572
|
+
items.push({
|
|
24573
|
+
key: "server_reachable",
|
|
24574
|
+
label: "Server reachable",
|
|
24575
|
+
status: "error",
|
|
24576
|
+
value: "no"
|
|
24577
|
+
});
|
|
24578
|
+
}
|
|
24579
|
+
} catch {
|
|
24580
|
+
items.push({
|
|
24581
|
+
key: "server",
|
|
24582
|
+
label: "Server URL",
|
|
24583
|
+
status: "missing_required",
|
|
24584
|
+
hint: "--server <url> or FIRST_TREE_HUB_SERVER"
|
|
24585
|
+
});
|
|
24586
|
+
}
|
|
24587
|
+
const repoPath = await resolveContextTreeRepo(args.server);
|
|
24588
|
+
if (repoPath) items.push({
|
|
24589
|
+
key: "repo",
|
|
24590
|
+
label: "Context Tree repo",
|
|
24591
|
+
status: "ok",
|
|
24592
|
+
value: repoPath
|
|
24593
|
+
});
|
|
24594
|
+
else {
|
|
24595
|
+
const serverAvailable = items.some((i) => i.key === "server" && i.status === "ok");
|
|
24596
|
+
items.push({
|
|
24597
|
+
key: "repo",
|
|
24598
|
+
label: "Context Tree repo",
|
|
24599
|
+
status: "missing_required",
|
|
24600
|
+
hint: serverAvailable ? "auto-clone failed (check server Context Tree config and gh auth)" : "configure --server first (repo will be auto-cloned from server)"
|
|
24601
|
+
});
|
|
24602
|
+
}
|
|
24603
|
+
items.push(args.id ? {
|
|
24604
|
+
key: "id",
|
|
24605
|
+
label: "id",
|
|
24606
|
+
status: "ok",
|
|
24607
|
+
value: args.id
|
|
24608
|
+
} : {
|
|
24609
|
+
key: "id",
|
|
24610
|
+
label: "id",
|
|
24611
|
+
status: "missing_required",
|
|
24612
|
+
hint: "Member directory name"
|
|
24613
|
+
});
|
|
24614
|
+
items.push(args.type ? {
|
|
24615
|
+
key: "type",
|
|
24616
|
+
label: "type",
|
|
24617
|
+
status: "ok",
|
|
24618
|
+
value: args.type
|
|
24619
|
+
} : {
|
|
24620
|
+
key: "type",
|
|
24621
|
+
label: "type",
|
|
24622
|
+
status: "missing_required",
|
|
24623
|
+
hint: "human | personal_assistant | autonomous_agent"
|
|
24624
|
+
});
|
|
24625
|
+
items.push(args.role ? {
|
|
24626
|
+
key: "role",
|
|
24627
|
+
label: "role",
|
|
24628
|
+
status: "ok",
|
|
24629
|
+
value: args.role
|
|
24630
|
+
} : {
|
|
24631
|
+
key: "role",
|
|
24632
|
+
label: "role",
|
|
24633
|
+
status: "missing_required",
|
|
24634
|
+
hint: "e.g. \"Engineer\""
|
|
24635
|
+
});
|
|
24636
|
+
items.push(args.domains ? {
|
|
24637
|
+
key: "domains",
|
|
24638
|
+
label: "domains",
|
|
24639
|
+
status: "ok",
|
|
24640
|
+
value: args.domains
|
|
24641
|
+
} : {
|
|
24642
|
+
key: "domains",
|
|
24643
|
+
label: "domains",
|
|
24644
|
+
status: "missing_required",
|
|
24645
|
+
hint: "Comma-separated, e.g. \"backend,infra\""
|
|
24646
|
+
});
|
|
24647
|
+
items.push(args.displayName ? {
|
|
24648
|
+
key: "display_name",
|
|
24649
|
+
label: "display-name",
|
|
24650
|
+
status: "ok",
|
|
24651
|
+
value: args.displayName
|
|
24652
|
+
} : {
|
|
24653
|
+
key: "display_name",
|
|
24654
|
+
label: "display-name",
|
|
24655
|
+
status: "missing_optional",
|
|
24656
|
+
hint: `defaults to "${args.id ?? ""}"`
|
|
24657
|
+
});
|
|
24658
|
+
items.push(args.assistant ? {
|
|
24659
|
+
key: "assistant",
|
|
24660
|
+
label: "assistant",
|
|
24661
|
+
status: "ok",
|
|
24662
|
+
value: args.assistant
|
|
24663
|
+
} : {
|
|
24664
|
+
key: "assistant",
|
|
24665
|
+
label: "assistant",
|
|
24666
|
+
status: "missing_optional",
|
|
24667
|
+
hint: "Also create a personal_assistant"
|
|
24668
|
+
});
|
|
24669
|
+
items.push(args.feishuBotAppId ? {
|
|
24670
|
+
key: "feishu_bot",
|
|
24671
|
+
label: "feishu-bot-app-id",
|
|
24672
|
+
status: "ok",
|
|
24673
|
+
value: args.feishuBotAppId
|
|
24674
|
+
} : {
|
|
24675
|
+
key: "feishu_bot",
|
|
24676
|
+
label: "feishu-bot-app-id",
|
|
24677
|
+
status: "missing_optional",
|
|
24678
|
+
hint: "Feishu bot App ID for assistant"
|
|
24679
|
+
});
|
|
24680
|
+
if (args.id && repoPath) if (existsSync(join(repoPath, "members", args.id))) try {
|
|
24681
|
+
execSync(`git ls-files --error-unmatch members/${args.id}/NODE.md`, {
|
|
24682
|
+
cwd: repoPath,
|
|
24683
|
+
stdio: "pipe"
|
|
24684
|
+
});
|
|
24685
|
+
items.push({
|
|
24686
|
+
key: "conflict",
|
|
24687
|
+
label: `ID "${args.id}" availability`,
|
|
24688
|
+
status: "warning",
|
|
24689
|
+
value: "already exists (will overwrite)"
|
|
24690
|
+
});
|
|
24691
|
+
} catch {
|
|
24692
|
+
items.push({
|
|
24693
|
+
key: "conflict",
|
|
24694
|
+
label: `ID "${args.id}" availability`,
|
|
24695
|
+
status: "ok",
|
|
24696
|
+
value: "resuming (local files from previous run)"
|
|
24697
|
+
});
|
|
24698
|
+
}
|
|
24699
|
+
else items.push({
|
|
24700
|
+
key: "conflict",
|
|
24701
|
+
label: `ID "${args.id}" availability`,
|
|
24702
|
+
status: "ok",
|
|
24703
|
+
value: "available"
|
|
24704
|
+
});
|
|
24705
|
+
return items;
|
|
24706
|
+
}
|
|
24707
|
+
function formatCheckReport(items) {
|
|
24708
|
+
const lines = [];
|
|
24709
|
+
for (const item of items) {
|
|
24710
|
+
const icon = item.status === "ok" ? "✅" : item.status === "missing_required" ? "❌" : item.status === "error" ? "❌" : item.status === "warning" ? "⚠️" : "⬜";
|
|
24711
|
+
const valueStr = item.value ? ` ${item.value}` : "";
|
|
24712
|
+
const hintStr = item.hint ? ` (${item.hint})` : "";
|
|
24713
|
+
lines.push(` ${icon} ${item.label.padEnd(20)}${valueStr}${hintStr}`);
|
|
24714
|
+
}
|
|
24715
|
+
return lines.join("\n");
|
|
24716
|
+
}
|
|
24717
|
+
async function onboardCreate(args) {
|
|
24718
|
+
const repoPath = await resolveContextTreeRepo(args.server);
|
|
24719
|
+
if (!repoPath) throw new Error("Context Tree repo not available. Ensure --server is configured and the server is running.");
|
|
24720
|
+
const ghUsername = getGitHubUsername();
|
|
24721
|
+
const githubField = args.type === "human" ? ghUsername : null;
|
|
24722
|
+
const humanNodePath = join(repoPath, "members", args.id, "NODE.md");
|
|
24723
|
+
if (existsSync(humanNodePath) && isTrackedByGit(repoPath, join("members", args.id, "NODE.md"))) {
|
|
24724
|
+
process.stderr.write(`Member "${args.id}" already exists, skipping NODE.md creation.\n`);
|
|
24725
|
+
if (args.assistant) {
|
|
24726
|
+
const existingContent = readFileSync(humanNodePath, "utf-8");
|
|
24727
|
+
if (!existingContent.includes("delegate_mention")) {
|
|
24728
|
+
writeFileSync(humanNodePath, existingContent.replace(/^(---\n[\s\S]*?)(---)/m, `$1delegate_mention: ${args.assistant}\n$2`));
|
|
24729
|
+
process.stderr.write(`Updated delegate_mention → ${args.assistant}\n`);
|
|
24730
|
+
}
|
|
24731
|
+
}
|
|
24732
|
+
} else createMemberNodeMd(repoPath, {
|
|
24733
|
+
id: args.id,
|
|
24734
|
+
type: args.type,
|
|
24735
|
+
displayName: args.displayName ?? args.id,
|
|
24736
|
+
role: args.role,
|
|
24737
|
+
domains: args.domains.split(",").map((d) => d.trim()),
|
|
24738
|
+
owner: ghUsername,
|
|
24739
|
+
github: githubField,
|
|
24740
|
+
delegateMention: args.assistant ?? args.delegateMention ?? null
|
|
24741
|
+
});
|
|
24742
|
+
if (args.assistant) if (existsSync(join(repoPath, "members", args.id, args.assistant, "NODE.md")) && isTrackedByGit(repoPath, join("members", args.id, args.assistant, "NODE.md"))) process.stderr.write(`Assistant "${args.assistant}" already exists, skipping.\n`);
|
|
24743
|
+
else createMemberNodeMd(repoPath, {
|
|
24744
|
+
parentPath: join("members", args.id),
|
|
24745
|
+
id: args.assistant,
|
|
24746
|
+
type: "personal_assistant",
|
|
24747
|
+
displayName: args.assistant,
|
|
24748
|
+
role: `Personal Assistant to ${args.id}`,
|
|
24749
|
+
domains: ["message triage", "task coordination"],
|
|
24750
|
+
owner: ghUsername,
|
|
24751
|
+
github: null,
|
|
24752
|
+
delegateMention: null
|
|
24753
|
+
});
|
|
24754
|
+
try {
|
|
24755
|
+
execSync("npx -y first-tree verify", {
|
|
24756
|
+
cwd: repoPath,
|
|
24757
|
+
stdio: "pipe"
|
|
24758
|
+
});
|
|
24759
|
+
} catch (err) {
|
|
24760
|
+
const stderr = err instanceof Error && "stderr" in err ? err.stderr.toString() : "";
|
|
24761
|
+
const stdout = err instanceof Error && "stdout" in err ? err.stdout.toString() : "";
|
|
24762
|
+
const output = stderr || stdout || String(err);
|
|
24763
|
+
if (output.includes("VERSION") || output.includes("AGENT.md") || output.includes("Root NODE.md")) throw new Error("Context Tree repo is not properly initialized.\nRun 'context-tree init' in the repo first, or see:\n https://github.com/agent-team-foundation/first-tree\n\n" + output);
|
|
24764
|
+
throw new Error(`Verification failed:\n${output}`);
|
|
24765
|
+
}
|
|
24766
|
+
const baseBranch = `onboard/${args.id}`;
|
|
24767
|
+
let branch = baseBranch;
|
|
24768
|
+
const branchExists = (name) => {
|
|
24769
|
+
try {
|
|
24770
|
+
execSync(`git rev-parse --verify ${name}`, {
|
|
24771
|
+
cwd: repoPath,
|
|
24772
|
+
stdio: "pipe"
|
|
24773
|
+
});
|
|
24774
|
+
return true;
|
|
24775
|
+
} catch {
|
|
24776
|
+
return false;
|
|
24777
|
+
}
|
|
24778
|
+
};
|
|
24779
|
+
if (branchExists(branch)) branch = `${baseBranch}-${Date.now().toString(36)}`;
|
|
24780
|
+
try {
|
|
24781
|
+
execSync("git checkout main", {
|
|
24782
|
+
cwd: repoPath,
|
|
24783
|
+
stdio: "pipe"
|
|
24784
|
+
});
|
|
24785
|
+
} catch {
|
|
24786
|
+
try {
|
|
24787
|
+
execSync("git checkout master", {
|
|
24788
|
+
cwd: repoPath,
|
|
24789
|
+
stdio: "pipe"
|
|
24790
|
+
});
|
|
24791
|
+
} catch {}
|
|
24792
|
+
}
|
|
24793
|
+
execSync(`git checkout -b ${branch}`, {
|
|
24794
|
+
cwd: repoPath,
|
|
24795
|
+
stdio: "pipe"
|
|
24796
|
+
});
|
|
24797
|
+
execSync(`git add members/${args.id}`, {
|
|
24798
|
+
cwd: repoPath,
|
|
24799
|
+
stdio: "pipe"
|
|
24800
|
+
});
|
|
24801
|
+
execFileSync("git", [
|
|
24802
|
+
"commit",
|
|
24803
|
+
"-m",
|
|
24804
|
+
args.assistant ? `feat: onboard ${args.id} + ${args.assistant}` : `feat: onboard ${args.id}`
|
|
24805
|
+
], {
|
|
24806
|
+
cwd: repoPath,
|
|
24807
|
+
stdio: "pipe"
|
|
24808
|
+
});
|
|
24809
|
+
const pushToken = execSync("gh auth token", {
|
|
24810
|
+
encoding: "utf-8",
|
|
24811
|
+
stdio: "pipe"
|
|
24812
|
+
}).trim();
|
|
24813
|
+
const cleanRemote = execSync("git remote get-url origin", {
|
|
24814
|
+
cwd: repoPath,
|
|
24815
|
+
encoding: "utf-8",
|
|
24816
|
+
stdio: "pipe"
|
|
24817
|
+
}).trim();
|
|
24818
|
+
execSync(`git remote set-url origin "${cleanRemote.replace("https://github.com/", `https://x-access-token:${pushToken}@github.com/`)}"`, {
|
|
24819
|
+
cwd: repoPath,
|
|
24820
|
+
stdio: "pipe"
|
|
24821
|
+
});
|
|
24822
|
+
try {
|
|
24823
|
+
execSync(`git push -u origin ${branch}`, {
|
|
24824
|
+
cwd: repoPath,
|
|
24825
|
+
stdio: "pipe"
|
|
24826
|
+
});
|
|
24827
|
+
} finally {
|
|
24828
|
+
execSync(`git remote set-url origin "${cleanRemote}"`, {
|
|
24829
|
+
cwd: repoPath,
|
|
24830
|
+
stdio: "pipe"
|
|
24831
|
+
});
|
|
24832
|
+
}
|
|
24833
|
+
const prOutput = execSync(`gh pr create --title "${args.assistant ? `Onboard ${args.id} + assistant` : `Onboard ${args.id}`}" --body "Automated onboard via first-tree-hub CLI"`, {
|
|
24834
|
+
cwd: repoPath,
|
|
24835
|
+
encoding: "utf-8"
|
|
24836
|
+
}).trim();
|
|
24837
|
+
const state = {
|
|
24838
|
+
args,
|
|
24839
|
+
branch,
|
|
24840
|
+
prUrl: prOutput
|
|
24841
|
+
};
|
|
24842
|
+
mkdirSync(join(homedir(), ".first-tree-hub"), { recursive: true });
|
|
24843
|
+
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
24844
|
+
return { prUrl: prOutput };
|
|
24845
|
+
}
|
|
24846
|
+
async function onboardContinue(args) {
|
|
24847
|
+
let state = null;
|
|
24848
|
+
try {
|
|
24849
|
+
state = JSON.parse(readFileSync(STATE_FILE, "utf-8"));
|
|
24850
|
+
} catch {}
|
|
24851
|
+
if (!state && !args.id) throw new Error("No onboard in progress. Run 'first-tree-hub onboard' first to start a new onboard.");
|
|
24852
|
+
const mergedArgs = {
|
|
24853
|
+
...state?.args,
|
|
24854
|
+
...stripUndefined(args)
|
|
24855
|
+
};
|
|
24856
|
+
const serverUrl = resolveServerUrl(mergedArgs.server).replace(/\/+$/, "");
|
|
24857
|
+
const agentToBootstrap = mergedArgs.assistant ?? mergedArgs.id;
|
|
24858
|
+
if (!agentToBootstrap) throw new Error("Cannot determine which agent to bootstrap. Provide --id or run onboard first.");
|
|
24859
|
+
if (!mergedArgs.id) throw new Error("Cannot determine member ID. Provide --id or run onboard first.");
|
|
24860
|
+
process.stderr.write(`Waiting for agent "${agentToBootstrap}" to be synced...\n`);
|
|
24861
|
+
let synced = false;
|
|
24862
|
+
for (let i = 0; i < 30; i++) {
|
|
24863
|
+
try {
|
|
24864
|
+
const status = await checkBootstrapStatus(serverUrl, agentToBootstrap);
|
|
24865
|
+
if (status.exists && status.status === "active") {
|
|
24866
|
+
synced = true;
|
|
24867
|
+
break;
|
|
24868
|
+
}
|
|
24869
|
+
} catch (err) {
|
|
24870
|
+
if (i === 0) process.stderr.write(` (check failed: ${err instanceof Error ? err.message : String(err)})\n`);
|
|
24871
|
+
}
|
|
24872
|
+
await sleep(2e3);
|
|
24873
|
+
}
|
|
24874
|
+
if (!synced) throw new Error(`Agent "${agentToBootstrap}" not found after 60s. Trigger sync manually or wait for auto-sync.`);
|
|
24875
|
+
process.stderr.write(`Bootstrapping token for "${agentToBootstrap}"...\n`);
|
|
24876
|
+
let token;
|
|
24877
|
+
try {
|
|
24878
|
+
token = (await bootstrapToken$1(serverUrl, agentToBootstrap, { saveTo: "agent" })).token;
|
|
24879
|
+
} catch (err) {
|
|
24880
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
24881
|
+
if (msg.includes("already has") || msg.includes("409")) throw new Error(`Agent "${agentToBootstrap}" already has an active token.\nAsk an admin to revoke the existing token in the Web UI, then re-run:
|
|
24882
|
+
first-tree-hub onboard --continue`);
|
|
24883
|
+
throw err;
|
|
24884
|
+
}
|
|
24885
|
+
process.stderr.write(`Token saved to ~/.first-tree-hub/agents/${agentToBootstrap}/agent.yaml\n`);
|
|
24886
|
+
if (mergedArgs.feishuBotAppId && mergedArgs.feishuBotAppSecret) {
|
|
24887
|
+
const { bindFeishuBot } = await import("./feishu-Y4m2zFc3.mjs").then((n) => n.r);
|
|
24888
|
+
process.stderr.write("Binding Feishu bot...\n");
|
|
24889
|
+
await bindFeishuBot(serverUrl, token, mergedArgs.feishuBotAppId, mergedArgs.feishuBotAppSecret);
|
|
24890
|
+
process.stderr.write("Feishu bot bound.\n");
|
|
24891
|
+
}
|
|
24892
|
+
try {
|
|
24893
|
+
const { unlinkSync } = await import("node:fs");
|
|
24894
|
+
unlinkSync(STATE_FILE);
|
|
24895
|
+
} catch {}
|
|
24896
|
+
process.stderr.write("\n✅ Onboard complete!\n\n");
|
|
24897
|
+
process.stderr.write(` Human: ${mergedArgs.id}\n`);
|
|
24898
|
+
if (mergedArgs.assistant) process.stderr.write(` Assistant: ${mergedArgs.assistant}\n`);
|
|
24899
|
+
process.stderr.write(` Token: ~/.first-tree-hub/agents/${agentToBootstrap}/agent.yaml\n`);
|
|
24900
|
+
if (mergedArgs.feishuBotAppId) process.stderr.write(` Feishu: bot bound (${mergedArgs.feishuBotAppId})\n`);
|
|
24901
|
+
if (mergedArgs.type === "human") {
|
|
24902
|
+
process.stderr.write("\n Next step — bind your Feishu account:\n");
|
|
24903
|
+
process.stderr.write(` Send this message to the bot in Feishu: /bind ${mergedArgs.id}\n`);
|
|
24904
|
+
if (!mergedArgs.feishuBotAppId) process.stderr.write(" (requires a Feishu bot to be configured in the system)\n");
|
|
24905
|
+
}
|
|
24906
|
+
process.stderr.write("\n");
|
|
24907
|
+
}
|
|
24908
|
+
function createMemberNodeMd(repoPath, data) {
|
|
24909
|
+
const memberDir = join(repoPath, data.parentPath ?? "members", data.id);
|
|
24910
|
+
mkdirSync(memberDir, { recursive: true });
|
|
24911
|
+
const domainsList = data.domains.map((d) => ` - "${d}"`).join("\n");
|
|
24912
|
+
const githubLine = data.github ? `\ngithub: ${data.github}` : "";
|
|
24913
|
+
const delegateLine = data.delegateMention ? `\ndelegate_mention: ${data.delegateMention}` : "";
|
|
24914
|
+
const content = `---
|
|
24915
|
+
title: "${data.displayName}"
|
|
24916
|
+
owners: [${data.owner}]
|
|
24917
|
+
type: ${data.type}
|
|
24918
|
+
role: "${data.role}"
|
|
24919
|
+
domains:
|
|
24920
|
+
${domainsList}${githubLine}${delegateLine}
|
|
24921
|
+
---
|
|
24922
|
+
|
|
24923
|
+
# ${data.displayName}
|
|
24924
|
+
|
|
24925
|
+
## About
|
|
24926
|
+
|
|
24927
|
+
## Current Focus
|
|
24928
|
+
`;
|
|
24929
|
+
writeFileSync(join(memberDir, "NODE.md"), content);
|
|
24930
|
+
}
|
|
24931
|
+
function isTrackedByGit(repoPath, filePath) {
|
|
24932
|
+
try {
|
|
24933
|
+
execSync(`git ls-files --error-unmatch ${filePath}`, {
|
|
24934
|
+
cwd: repoPath,
|
|
24935
|
+
stdio: "pipe"
|
|
24936
|
+
});
|
|
24937
|
+
return true;
|
|
24938
|
+
} catch {
|
|
24939
|
+
return false;
|
|
24940
|
+
}
|
|
24941
|
+
}
|
|
24942
|
+
const CONTEXT_TREE_DIR = join(homedir(), ".first-tree-hub", "context-tree");
|
|
24943
|
+
/**
|
|
24944
|
+
* Resolve Context Tree to a **local path** at ~/.first-tree-hub/context-tree/.
|
|
24945
|
+
*
|
|
24946
|
+
* Repo URL is obtained from the Hub server. The local clone is always
|
|
24947
|
+
* managed in the standard location — no custom paths allowed.
|
|
24948
|
+
*/
|
|
24949
|
+
async function resolveContextTreeRepo(serverUrl) {
|
|
24950
|
+
const repoUrl = await fetchRepoUrlFromServer(serverUrl);
|
|
24951
|
+
if (!repoUrl) return null;
|
|
24952
|
+
let ghToken;
|
|
24953
|
+
try {
|
|
24954
|
+
ghToken = execSync("gh auth token", {
|
|
24955
|
+
encoding: "utf-8",
|
|
24956
|
+
stdio: "pipe"
|
|
24957
|
+
}).trim();
|
|
24958
|
+
} catch {
|
|
24959
|
+
return null;
|
|
24960
|
+
}
|
|
24961
|
+
const gitEnv = {
|
|
24962
|
+
...process.env,
|
|
24963
|
+
GIT_ASKPASS: "echo",
|
|
24964
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
24965
|
+
GH_TOKEN: ghToken,
|
|
24966
|
+
GITHUB_TOKEN: ghToken
|
|
24967
|
+
};
|
|
24968
|
+
const gitConfigArgs = `-c url."https://x-access-token:${ghToken}@github.com/".insteadOf="https://github.com/"`;
|
|
24969
|
+
if (existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
|
|
24970
|
+
try {
|
|
24971
|
+
if (execSync("git remote get-url origin", {
|
|
24972
|
+
cwd: CONTEXT_TREE_DIR,
|
|
24973
|
+
encoding: "utf-8",
|
|
24974
|
+
stdio: "pipe"
|
|
24975
|
+
}).trim().includes(repoUrl.replace(/^https?:\/\/github\.com\//, "").replace(/\.git$/, ""))) {
|
|
24976
|
+
process.stderr.write("Updating Context Tree...\n");
|
|
24977
|
+
execSync("git checkout main 2>/dev/null || git checkout master", {
|
|
24978
|
+
cwd: CONTEXT_TREE_DIR,
|
|
24979
|
+
stdio: "pipe"
|
|
24980
|
+
});
|
|
24981
|
+
try {
|
|
24982
|
+
execSync(`git ${gitConfigArgs} pull --ff-only`, {
|
|
24983
|
+
cwd: CONTEXT_TREE_DIR,
|
|
24984
|
+
stdio: "pipe",
|
|
24985
|
+
env: gitEnv
|
|
24986
|
+
});
|
|
24987
|
+
} catch {}
|
|
24988
|
+
return CONTEXT_TREE_DIR;
|
|
24989
|
+
}
|
|
24990
|
+
} catch {}
|
|
24991
|
+
const safePrefix = join(homedir(), ".first-tree-hub");
|
|
24992
|
+
if (!CONTEXT_TREE_DIR.startsWith(safePrefix) || CONTEXT_TREE_DIR === safePrefix) throw new Error(`Refusing to delete unsafe path: ${CONTEXT_TREE_DIR}`);
|
|
24993
|
+
execSync(`rm -rf ${CONTEXT_TREE_DIR}`);
|
|
24994
|
+
}
|
|
24995
|
+
try {
|
|
24996
|
+
process.stderr.write(`Cloning Context Tree to ${CONTEXT_TREE_DIR}...\n`);
|
|
24997
|
+
mkdirSync(join(homedir(), ".first-tree-hub"), { recursive: true });
|
|
24998
|
+
execSync(`git ${gitConfigArgs} clone ${repoUrl} ${CONTEXT_TREE_DIR}`, {
|
|
24999
|
+
stdio: "pipe",
|
|
25000
|
+
env: gitEnv
|
|
25001
|
+
});
|
|
25002
|
+
return CONTEXT_TREE_DIR;
|
|
25003
|
+
} catch {
|
|
25004
|
+
return null;
|
|
25005
|
+
}
|
|
25006
|
+
}
|
|
25007
|
+
/** Query server for Context Tree repo URL. */
|
|
25008
|
+
async function fetchRepoUrlFromServer(serverUrl) {
|
|
25009
|
+
if (!serverUrl) try {
|
|
25010
|
+
serverUrl = resolveServerUrl();
|
|
25011
|
+
} catch {
|
|
25012
|
+
return null;
|
|
25013
|
+
}
|
|
25014
|
+
try {
|
|
25015
|
+
const url = `${serverUrl.replace(/\/+$/, "")}/api/v1/context-tree/info`;
|
|
25016
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5e3) });
|
|
25017
|
+
if (!res.ok) return null;
|
|
25018
|
+
return (await res.json()).repo ?? null;
|
|
25019
|
+
} catch {
|
|
25020
|
+
return null;
|
|
25021
|
+
}
|
|
25022
|
+
}
|
|
25023
|
+
function sleep(ms) {
|
|
25024
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
25025
|
+
}
|
|
25026
|
+
function stripUndefined(obj) {
|
|
25027
|
+
const result = {};
|
|
25028
|
+
for (const [key, value] of Object.entries(obj)) if (value !== void 0) result[key] = value;
|
|
25029
|
+
return result;
|
|
25030
|
+
}
|
|
25031
|
+
//#endregion
|
|
24977
25032
|
//#region src/core/prompt.ts
|
|
24978
25033
|
/**
|
|
24979
25034
|
* Check if interactive mode is available.
|
|
@@ -25113,6 +25168,10 @@ const adapterBindMethodSchema = z.enum([
|
|
|
25113
25168
|
"oauth",
|
|
25114
25169
|
"manual"
|
|
25115
25170
|
]);
|
|
25171
|
+
const selfServiceFeishuBotSchema = z.object({
|
|
25172
|
+
appId: z.string().min(1),
|
|
25173
|
+
appSecret: z.string().min(1)
|
|
25174
|
+
});
|
|
25116
25175
|
const createAdapterMappingSchema = z.object({
|
|
25117
25176
|
platform: adapterPlatformSchema,
|
|
25118
25177
|
externalUserId: z.string().min(1),
|
|
@@ -25129,6 +25188,10 @@ z.object({
|
|
|
25129
25188
|
displayName: z.string().nullable(),
|
|
25130
25189
|
createdAt: z.string()
|
|
25131
25190
|
});
|
|
25191
|
+
const delegateFeishuUserSchema = z.object({
|
|
25192
|
+
feishuUserId: z.string().min(1),
|
|
25193
|
+
displayName: z.string().max(200).optional()
|
|
25194
|
+
});
|
|
25132
25195
|
z.object({
|
|
25133
25196
|
configId: z.number(),
|
|
25134
25197
|
platform: z.string(),
|
|
@@ -25220,6 +25283,16 @@ z.object({
|
|
|
25220
25283
|
createdAt: z.string(),
|
|
25221
25284
|
updatedAt: z.string()
|
|
25222
25285
|
});
|
|
25286
|
+
const bootstrapTokenRequestSchema = z.object({ name: z.string().max(100).optional() });
|
|
25287
|
+
z.object({
|
|
25288
|
+
exists: z.boolean(),
|
|
25289
|
+
status: z.enum(["active", "suspended"]).nullable()
|
|
25290
|
+
});
|
|
25291
|
+
z.object({
|
|
25292
|
+
repo: z.string(),
|
|
25293
|
+
branch: z.string(),
|
|
25294
|
+
lastSync: z.string().nullable()
|
|
25295
|
+
});
|
|
25223
25296
|
const createAgentTokenSchema = z.object({
|
|
25224
25297
|
name: z.string().max(100).optional(),
|
|
25225
25298
|
expiresAt: z.string().datetime().optional()
|
|
@@ -25340,7 +25413,7 @@ const SYSTEM_CONFIG_DEFAULTS = {
|
|
|
25340
25413
|
[SYSTEM_CONFIG_KEYS.PRESENCE_CLEANUP_SECONDS]: 60
|
|
25341
25414
|
};
|
|
25342
25415
|
//#endregion
|
|
25343
|
-
//#region ../server/dist/app-
|
|
25416
|
+
//#region ../server/dist/app-BVTDWxJE.mjs
|
|
25344
25417
|
var __defProp = Object.defineProperty;
|
|
25345
25418
|
var __exportAll = (all, no_symbols) => {
|
|
25346
25419
|
let target = {};
|
|
@@ -25493,6 +25566,11 @@ async function findAgentByExternalUser(db, platform, externalUserId) {
|
|
|
25493
25566
|
}).from(adapterAgentMappings).where(and(eq(adapterAgentMappings.platform, platform), eq(adapterAgentMappings.externalUserId, externalUserId))).limit(1);
|
|
25494
25567
|
return row ?? null;
|
|
25495
25568
|
}
|
|
25569
|
+
/** Look up the external user ID for an internal agent. */
|
|
25570
|
+
async function findExternalUserByAgent(db, platform, agentId) {
|
|
25571
|
+
const [row] = await db.select({ externalUserId: adapterAgentMappings.externalUserId }).from(adapterAgentMappings).where(and(eq(adapterAgentMappings.platform, platform), eq(adapterAgentMappings.agentId, agentId))).limit(1);
|
|
25572
|
+
return row ?? null;
|
|
25573
|
+
}
|
|
25496
25574
|
/** Create an agent mapping. */
|
|
25497
25575
|
async function createAgentMapping(db, data) {
|
|
25498
25576
|
const [row] = await db.insert(adapterAgentMappings).values({
|
|
@@ -25993,17 +26071,27 @@ function parseNodeMetadata(content) {
|
|
|
25993
26071
|
if (!match) return {
|
|
25994
26072
|
type: "autonomous_agent",
|
|
25995
26073
|
displayName: null,
|
|
25996
|
-
delegateMention: null
|
|
26074
|
+
delegateMention: null,
|
|
26075
|
+
owners: [],
|
|
26076
|
+
github: null
|
|
25997
26077
|
};
|
|
25998
26078
|
const frontmatter = match[1] ?? "";
|
|
25999
26079
|
const getValue = (key) => {
|
|
26000
26080
|
const lineMatch = new RegExp(`^${key}:\\s*(.+)$`, "m").exec(frontmatter);
|
|
26001
26081
|
return lineMatch ? lineMatch[1]?.trim().replace(/^["']|["']$/g, "") ?? null : null;
|
|
26002
26082
|
};
|
|
26083
|
+
const ownersRaw = getValue("owners");
|
|
26084
|
+
let owners = [];
|
|
26085
|
+
if (ownersRaw) {
|
|
26086
|
+
const listMatch = /^\[([^\]]*)\]$/.exec(ownersRaw);
|
|
26087
|
+
if (listMatch?.[1]) owners = listMatch[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
26088
|
+
}
|
|
26003
26089
|
return {
|
|
26004
26090
|
type: getValue("type") ?? "autonomous_agent",
|
|
26005
26091
|
displayName: getValue("display_name") ?? getValue("title") ?? getValue("name"),
|
|
26006
|
-
delegateMention: getValue("delegate_mention")
|
|
26092
|
+
delegateMention: getValue("delegate_mention"),
|
|
26093
|
+
owners,
|
|
26094
|
+
github: getValue("github")
|
|
26007
26095
|
};
|
|
26008
26096
|
}
|
|
26009
26097
|
/** Stored for the /status endpoint */
|
|
@@ -26037,26 +26125,38 @@ async function syncFromGitHub(db, repo, branch, githubToken) {
|
|
|
26037
26125
|
const meta = member.nodeContent ? parseNodeMetadata(member.nodeContent) : {
|
|
26038
26126
|
type: "autonomous_agent",
|
|
26039
26127
|
displayName: null,
|
|
26040
|
-
delegateMention: null
|
|
26128
|
+
delegateMention: null,
|
|
26129
|
+
owners: [],
|
|
26130
|
+
github: null
|
|
26041
26131
|
};
|
|
26042
|
-
const
|
|
26132
|
+
const metadataJson = JSON.stringify({
|
|
26133
|
+
owners: meta.owners,
|
|
26134
|
+
github: meta.github
|
|
26135
|
+
});
|
|
26136
|
+
const existing = await db.execute(sql`SELECT id, status, type, display_name, delegate_mention, tree_path, metadata FROM agents WHERE id = ${member.name}`);
|
|
26043
26137
|
if (existing.length === 0) {
|
|
26044
26138
|
await db.execute(sql`
|
|
26045
|
-
INSERT INTO agents (id, type, display_name, delegate_mention, tree_path, status, inbox_id)
|
|
26046
|
-
VALUES (${member.name}, ${meta.type}, ${meta.displayName}, ${meta.delegateMention}, ${member.treePath}, 'active', ${`inbox_${member.name}`})
|
|
26139
|
+
INSERT INTO agents (id, type, display_name, delegate_mention, tree_path, status, inbox_id, metadata)
|
|
26140
|
+
VALUES (${member.name}, ${meta.type}, ${meta.displayName}, ${meta.delegateMention}, ${member.treePath}, 'active', ${`inbox_${member.name}`}, ${metadataJson}::jsonb)
|
|
26047
26141
|
`);
|
|
26048
26142
|
result.created++;
|
|
26049
26143
|
} else {
|
|
26050
26144
|
const agent = existing[0];
|
|
26051
|
-
|
|
26145
|
+
const existingMeta = agent.metadata ?? {};
|
|
26146
|
+
const mergedMeta = JSON.stringify({
|
|
26147
|
+
...existingMeta,
|
|
26148
|
+
owners: meta.owners,
|
|
26149
|
+
github: meta.github
|
|
26150
|
+
});
|
|
26151
|
+
if (agent.status === "suspended" || agent.status === "deleted") {
|
|
26052
26152
|
await db.execute(sql`
|
|
26053
|
-
UPDATE agents SET status = 'active', type = ${meta.type}, display_name = ${meta.displayName}, delegate_mention = ${meta.delegateMention}, tree_path = ${member.treePath}
|
|
26153
|
+
UPDATE agents SET status = 'active', type = ${meta.type}, display_name = ${meta.displayName}, delegate_mention = ${meta.delegateMention}, tree_path = ${member.treePath}, metadata = ${mergedMeta}::jsonb
|
|
26054
26154
|
WHERE id = ${member.name}
|
|
26055
26155
|
`);
|
|
26056
26156
|
result.reactivated++;
|
|
26057
|
-
} else if (agent.type !== meta.type || agent.display_name !== meta.displayName || agent.delegate_mention !== meta.delegateMention || agent.tree_path !== member.treePath) {
|
|
26157
|
+
} else if (agent.type !== meta.type || agent.display_name !== meta.displayName || agent.delegate_mention !== meta.delegateMention || agent.tree_path !== member.treePath || JSON.stringify(existingMeta.owners) !== JSON.stringify(meta.owners) || (existingMeta.github ?? null) !== (meta.github ?? null)) {
|
|
26058
26158
|
await db.execute(sql`
|
|
26059
|
-
UPDATE agents SET type = ${meta.type}, display_name = ${meta.displayName}, delegate_mention = ${meta.delegateMention}, tree_path = ${member.treePath}
|
|
26159
|
+
UPDATE agents SET type = ${meta.type}, display_name = ${meta.displayName}, delegate_mention = ${meta.delegateMention}, tree_path = ${member.treePath}, metadata = ${mergedMeta}::jsonb
|
|
26060
26160
|
WHERE id = ${member.name}
|
|
26061
26161
|
`);
|
|
26062
26162
|
result.updated++;
|
|
@@ -26203,6 +26303,17 @@ async function deleteAgent(db, id) {
|
|
|
26203
26303
|
if (!agent) throw new Error("Unexpected: UPDATE RETURNING produced no row");
|
|
26204
26304
|
return agent;
|
|
26205
26305
|
}
|
|
26306
|
+
/**
|
|
26307
|
+
* Bootstrap a token for an agent using GitHub identity.
|
|
26308
|
+
* Only works when the agent has no active (non-revoked, non-expired) tokens.
|
|
26309
|
+
*/
|
|
26310
|
+
async function bootstrapToken(db, agentId, githubUsername, tokenName) {
|
|
26311
|
+
const agent = await getAgent(db, agentId);
|
|
26312
|
+
if (!(Array.isArray(agent.metadata?.owners) ? agent.metadata.owners : []).includes(githubUsername)) throw new ForbiddenError(`GitHub user "${githubUsername}" is not in the owners list for agent "${agentId}"`);
|
|
26313
|
+
const activeTokens = await db.select({ id: agentTokens.id }).from(agentTokens).where(and(eq(agentTokens.agentId, agentId), isNull(agentTokens.revokedAt)));
|
|
26314
|
+
if (activeTokens.length > 0) throw new ConflictError(`Agent "${agentId}" already has ${activeTokens.length} active token(s). Revoke all tokens first to re-bootstrap.`);
|
|
26315
|
+
return createToken(db, agentId, { name: tokenName ?? "bootstrap" });
|
|
26316
|
+
}
|
|
26206
26317
|
async function createToken(db, agentId, data) {
|
|
26207
26318
|
await getAgent(db, agentId);
|
|
26208
26319
|
const raw = `aghub_${randomBytes(32).toString("hex")}`;
|
|
@@ -27095,6 +27206,96 @@ async function agentContextTreeRoutes(app) {
|
|
|
27095
27206
|
});
|
|
27096
27207
|
});
|
|
27097
27208
|
}
|
|
27209
|
+
async function agentFeishuBotRoutes(app) {
|
|
27210
|
+
/**
|
|
27211
|
+
* PUT /agent/me/feishu-bot
|
|
27212
|
+
* Self-service: agent binds its own Feishu bot (upsert).
|
|
27213
|
+
*/
|
|
27214
|
+
app.put("/me/feishu-bot", async (request, reply) => {
|
|
27215
|
+
const identity = requireAgent(request);
|
|
27216
|
+
const body = selfServiceFeishuBotSchema.parse(request.body);
|
|
27217
|
+
if ((await getAgent(app.db, identity.id)).type === "human") throw new BadRequestError("Human agents cannot bind Feishu bots. Use bind-user instead.");
|
|
27218
|
+
const current = (await listAdapterConfigs(app.db)).find((c) => c.agentId === identity.id && c.platform === "feishu");
|
|
27219
|
+
let config;
|
|
27220
|
+
if (current) config = await updateAdapterConfig(app.db, current.id, {
|
|
27221
|
+
credentials: {
|
|
27222
|
+
app_id: body.appId,
|
|
27223
|
+
app_secret: body.appSecret
|
|
27224
|
+
},
|
|
27225
|
+
status: "active"
|
|
27226
|
+
}, app.config.secrets.encryptionKey);
|
|
27227
|
+
else config = await createAdapterConfig(app.db, {
|
|
27228
|
+
platform: "feishu",
|
|
27229
|
+
agentId: identity.id,
|
|
27230
|
+
credentials: {
|
|
27231
|
+
app_id: body.appId,
|
|
27232
|
+
app_secret: body.appSecret
|
|
27233
|
+
},
|
|
27234
|
+
status: "active"
|
|
27235
|
+
}, app.config.secrets.encryptionKey);
|
|
27236
|
+
app.adapterManager.reload().catch((err) => app.log.error(err, "Adapter reload failed after self-service bind"));
|
|
27237
|
+
app.notifier.notifyConfigChange("adapter_configs").catch(() => {});
|
|
27238
|
+
return reply.status(current ? 200 : 201).send({
|
|
27239
|
+
...config,
|
|
27240
|
+
createdAt: config.createdAt.toISOString(),
|
|
27241
|
+
updatedAt: config.updatedAt.toISOString()
|
|
27242
|
+
});
|
|
27243
|
+
});
|
|
27244
|
+
/**
|
|
27245
|
+
* DELETE /agent/me/feishu-bot
|
|
27246
|
+
* Self-service: agent unbinds its own Feishu bot.
|
|
27247
|
+
*/
|
|
27248
|
+
app.delete("/me/feishu-bot", async (request, reply) => {
|
|
27249
|
+
const identity = requireAgent(request);
|
|
27250
|
+
const current = (await listAdapterConfigs(app.db)).find((c) => c.agentId === identity.id && c.platform === "feishu");
|
|
27251
|
+
if (!current) return reply.status(204).send();
|
|
27252
|
+
await deleteAdapterConfig(app.db, current.id);
|
|
27253
|
+
app.adapterManager.reload().catch((err) => app.log.error(err, "Adapter reload failed after self-service unbind"));
|
|
27254
|
+
app.notifier.notifyConfigChange("adapter_configs").catch(() => {});
|
|
27255
|
+
return reply.status(204).send();
|
|
27256
|
+
});
|
|
27257
|
+
}
|
|
27258
|
+
async function agentFeishuUserRoutes(app) {
|
|
27259
|
+
/**
|
|
27260
|
+
* POST /agent/delegated/:humanAgentId/feishu-user
|
|
27261
|
+
* Assistant binds its owner's Feishu user ID via delegate_mention authorization.
|
|
27262
|
+
*/
|
|
27263
|
+
app.post("/:humanAgentId/feishu-user", async (request, reply) => {
|
|
27264
|
+
const identity = requireAgent(request);
|
|
27265
|
+
const { humanAgentId } = request.params;
|
|
27266
|
+
const body = delegateFeishuUserSchema.parse(request.body);
|
|
27267
|
+
const humanAgent = await getAgent(app.db, humanAgentId);
|
|
27268
|
+
if (humanAgent.type !== "human") throw new BadRequestError(`Agent "${humanAgentId}" is not a human agent`);
|
|
27269
|
+
if (humanAgent.delegateMention !== identity.id) throw new ForbiddenError(`Agent "${identity.id}" is not the delegate of "${humanAgentId}". Expected delegate_mention="${identity.id}" but found "${humanAgent.delegateMention ?? "(none)"}".`);
|
|
27270
|
+
const mapping = await createAgentMapping(app.db, {
|
|
27271
|
+
platform: "feishu",
|
|
27272
|
+
externalUserId: body.feishuUserId,
|
|
27273
|
+
agentId: humanAgentId,
|
|
27274
|
+
boundVia: "delegate",
|
|
27275
|
+
displayName: body.displayName
|
|
27276
|
+
});
|
|
27277
|
+
return reply.status(201).send({
|
|
27278
|
+
id: mapping.id,
|
|
27279
|
+
platform: mapping.platform,
|
|
27280
|
+
externalUserId: mapping.externalUserId,
|
|
27281
|
+
agentId: mapping.agentId,
|
|
27282
|
+
boundVia: mapping.boundVia,
|
|
27283
|
+
displayName: mapping.displayName,
|
|
27284
|
+
createdAt: mapping.createdAt.toISOString()
|
|
27285
|
+
});
|
|
27286
|
+
});
|
|
27287
|
+
/**
|
|
27288
|
+
* DELETE /agent/delegated/:humanAgentId/feishu-user
|
|
27289
|
+
* Assistant unbinds its owner's Feishu user ID.
|
|
27290
|
+
*/
|
|
27291
|
+
app.delete("/:humanAgentId/feishu-user", async (request, reply) => {
|
|
27292
|
+
const identity = requireAgent(request);
|
|
27293
|
+
const { humanAgentId } = request.params;
|
|
27294
|
+
if ((await getAgent(app.db, humanAgentId)).delegateMention !== identity.id) throw new ForbiddenError(`Agent "${identity.id}" is not the delegate of "${humanAgentId}"`);
|
|
27295
|
+
await app.db.delete(adapterAgentMappings).where(and(eq(adapterAgentMappings.platform, "feishu"), eq(adapterAgentMappings.agentId, humanAgentId)));
|
|
27296
|
+
return reply.status(204).send();
|
|
27297
|
+
});
|
|
27298
|
+
}
|
|
27098
27299
|
const DEFAULT_INBOX_TIMEOUT_SECONDS = 300;
|
|
27099
27300
|
const DEFAULT_MAX_RETRY_COUNT = 3;
|
|
27100
27301
|
async function pollInbox(db, inboxId, limit) {
|
|
@@ -27288,6 +27489,62 @@ function agentWsRoutes(notifier, instanceId) {
|
|
|
27288
27489
|
});
|
|
27289
27490
|
};
|
|
27290
27491
|
}
|
|
27492
|
+
async function bootstrapRoutes(app) {
|
|
27493
|
+
/**
|
|
27494
|
+
* POST /bootstrap/:agentId/token
|
|
27495
|
+
* GitHub identity → Agent token.
|
|
27496
|
+
* Only works when the agent has no active tokens.
|
|
27497
|
+
*/
|
|
27498
|
+
app.post("/:agentId/token", async (request, reply) => {
|
|
27499
|
+
const { agentId } = request.params;
|
|
27500
|
+
const githubUser = request.githubUser;
|
|
27501
|
+
if (!githubUser) throw new ForbiddenError("GitHub authentication required");
|
|
27502
|
+
const body = bootstrapTokenRequestSchema.parse(request.body ?? {});
|
|
27503
|
+
const result = await bootstrapToken(app.db, agentId, githubUser.username, body.name);
|
|
27504
|
+
return reply.status(201).send({
|
|
27505
|
+
id: result.id,
|
|
27506
|
+
agentId: result.agentId,
|
|
27507
|
+
name: result.name,
|
|
27508
|
+
token: result.token,
|
|
27509
|
+
expiresAt: result.expiresAt?.toISOString() ?? null,
|
|
27510
|
+
createdAt: result.createdAt.toISOString()
|
|
27511
|
+
});
|
|
27512
|
+
});
|
|
27513
|
+
/**
|
|
27514
|
+
* GET /bootstrap/:agentId/status
|
|
27515
|
+
* Check if an agent exists and its status (for polling after PR merge + sync).
|
|
27516
|
+
*/
|
|
27517
|
+
app.get("/:agentId/status", async (request) => {
|
|
27518
|
+
const { agentId } = request.params;
|
|
27519
|
+
const githubUser = request.githubUser;
|
|
27520
|
+
if (!githubUser) throw new ForbiddenError("GitHub authentication required");
|
|
27521
|
+
try {
|
|
27522
|
+
const agent = await getAgent(app.db, agentId);
|
|
27523
|
+
if (!(Array.isArray(agent.metadata?.owners) ? agent.metadata.owners : []).includes(githubUser.username)) throw new ForbiddenError(`GitHub user "${githubUser.username}" is not in the owners list for agent "${agentId}"`);
|
|
27524
|
+
return {
|
|
27525
|
+
exists: true,
|
|
27526
|
+
status: agent.status
|
|
27527
|
+
};
|
|
27528
|
+
} catch (err) {
|
|
27529
|
+
if (err instanceof NotFoundError) return {
|
|
27530
|
+
exists: false,
|
|
27531
|
+
status: null
|
|
27532
|
+
};
|
|
27533
|
+
throw err;
|
|
27534
|
+
}
|
|
27535
|
+
});
|
|
27536
|
+
}
|
|
27537
|
+
async function contextTreeInfoRoutes(app) {
|
|
27538
|
+
/** Public endpoint — returns Context Tree repo metadata for CLI auto-discovery. */
|
|
27539
|
+
app.get("/info", async () => {
|
|
27540
|
+
const { repo, branch } = app.config.contextTree;
|
|
27541
|
+
return {
|
|
27542
|
+
repo,
|
|
27543
|
+
branch,
|
|
27544
|
+
lastSync: null
|
|
27545
|
+
};
|
|
27546
|
+
});
|
|
27547
|
+
}
|
|
27291
27548
|
async function healthRoutes(app) {
|
|
27292
27549
|
app.get("/health", async () => {
|
|
27293
27550
|
try {
|
|
@@ -27855,6 +28112,25 @@ function agentAuthHook(db) {
|
|
|
27855
28112
|
request.agent = agent;
|
|
27856
28113
|
};
|
|
27857
28114
|
}
|
|
28115
|
+
const GITHUB_API_URL = "https://api.github.com/user";
|
|
28116
|
+
/**
|
|
28117
|
+
* Middleware that validates a GitHub token from the `X-GitHub-Token` header.
|
|
28118
|
+
* On success, sets `request.githubUser = { username }`.
|
|
28119
|
+
*/
|
|
28120
|
+
function githubAuthHook() {
|
|
28121
|
+
return async (request, _reply) => {
|
|
28122
|
+
const token = request.headers["x-github-token"];
|
|
28123
|
+
if (!token || typeof token !== "string") throw new UnauthorizedError("Missing X-GitHub-Token header");
|
|
28124
|
+
const res = await fetch(GITHUB_API_URL, { headers: {
|
|
28125
|
+
Authorization: `Bearer ${token}`,
|
|
28126
|
+
Accept: "application/vnd.github+json"
|
|
28127
|
+
} });
|
|
28128
|
+
if (!res.ok) throw new UnauthorizedError("Invalid GitHub token");
|
|
28129
|
+
const data = await res.json();
|
|
28130
|
+
if (!data.login) throw new ForbiddenError("Could not determine GitHub username from token");
|
|
28131
|
+
request.githubUser = { username: data.login };
|
|
28132
|
+
};
|
|
28133
|
+
}
|
|
27858
28134
|
const PROXY_ENV_KEYS = [
|
|
27859
28135
|
"HTTP_PROXY",
|
|
27860
28136
|
"HTTPS_PROXY",
|
|
@@ -28088,6 +28364,12 @@ function parseEventData(appId, data) {
|
|
|
28088
28364
|
};
|
|
28089
28365
|
}
|
|
28090
28366
|
async function processInboundMessage(db, event, bot, log, inboxNotifier) {
|
|
28367
|
+
const messageText = extractTextContent(event);
|
|
28368
|
+
const bindMatch = /^\/bind\s+(\S+)/.exec(messageText);
|
|
28369
|
+
if (bindMatch?.[1]) {
|
|
28370
|
+
await handleBindCommand(db, bot, event, bindMatch[1], log);
|
|
28371
|
+
return;
|
|
28372
|
+
}
|
|
28091
28373
|
const agentMapping = await findAgentByExternalUser(db, "feishu", event.senderId);
|
|
28092
28374
|
if (!agentMapping) {
|
|
28093
28375
|
await replyUnknownUser(bot, event, log);
|
|
@@ -28130,8 +28412,9 @@ async function processInboundMessage(db, event, bot, log, inboxNotifier) {
|
|
|
28130
28412
|
async function replyUnknownUser(bot, event, log) {
|
|
28131
28413
|
const text = [
|
|
28132
28414
|
"Your account is not linked to First Tree Hub yet.",
|
|
28133
|
-
"
|
|
28134
|
-
|
|
28415
|
+
"To bind, send: /bind <your-agent-id>",
|
|
28416
|
+
"",
|
|
28417
|
+
"Example: /bind alice"
|
|
28135
28418
|
].join("\n");
|
|
28136
28419
|
try {
|
|
28137
28420
|
await botApiCall(bot, () => bot.client.im.v1.message.create({
|
|
@@ -28149,6 +28432,81 @@ async function replyUnknownUser(bot, event, log) {
|
|
|
28149
28432
|
}, "Failed to send unknown-user reply");
|
|
28150
28433
|
}
|
|
28151
28434
|
}
|
|
28435
|
+
/** Extract plain text from a Feishu message event. */
|
|
28436
|
+
function extractTextContent(event) {
|
|
28437
|
+
if (typeof event.content === "object" && event.content !== null && "text" in event.content) return (event.content.text ?? "").trim();
|
|
28438
|
+
return "";
|
|
28439
|
+
}
|
|
28440
|
+
/**
|
|
28441
|
+
* Handle `/bind <agentId>` command from Feishu.
|
|
28442
|
+
* Binds the sender's Feishu user ID to the specified human agent.
|
|
28443
|
+
*/
|
|
28444
|
+
async function handleBindCommand(db, bot, event, agentId, log) {
|
|
28445
|
+
const reply = async (text) => {
|
|
28446
|
+
try {
|
|
28447
|
+
await botApiCall(bot, () => bot.client.im.v1.message.create({
|
|
28448
|
+
params: { receive_id_type: "chat_id" },
|
|
28449
|
+
data: {
|
|
28450
|
+
receive_id: event.externalChannelId,
|
|
28451
|
+
msg_type: "text",
|
|
28452
|
+
content: JSON.stringify({ text })
|
|
28453
|
+
}
|
|
28454
|
+
}));
|
|
28455
|
+
} catch (err) {
|
|
28456
|
+
log.warn({ err }, "Failed to send /bind reply");
|
|
28457
|
+
}
|
|
28458
|
+
};
|
|
28459
|
+
const existingMapping = await findAgentByExternalUser(db, "feishu", event.senderId);
|
|
28460
|
+
if (existingMapping) {
|
|
28461
|
+
await reply(`You are already bound to agent "${existingMapping.agentId}". Unbind first if you want to rebind.`);
|
|
28462
|
+
return;
|
|
28463
|
+
}
|
|
28464
|
+
const [agent] = await db.select({
|
|
28465
|
+
id: agents.id,
|
|
28466
|
+
type: agents.type,
|
|
28467
|
+
status: agents.status
|
|
28468
|
+
}).from(agents).where(eq(agents.id, agentId)).limit(1);
|
|
28469
|
+
if (!agent) {
|
|
28470
|
+
await reply(`Agent "${agentId}" not found. Check the ID and try again.`);
|
|
28471
|
+
return;
|
|
28472
|
+
}
|
|
28473
|
+
if (agent.status !== "active") {
|
|
28474
|
+
await reply(`Agent "${agentId}" is ${agent.status}. Only active agents can be bound.`);
|
|
28475
|
+
return;
|
|
28476
|
+
}
|
|
28477
|
+
if (agent.type !== "human") {
|
|
28478
|
+
await reply(`Agent "${agentId}" is not a human agent (type: ${agent.type}). Only human agents can bind Feishu users.`);
|
|
28479
|
+
return;
|
|
28480
|
+
}
|
|
28481
|
+
const existingAgentBinding = await findExternalUserByAgent(db, "feishu", agentId);
|
|
28482
|
+
if (existingAgentBinding) {
|
|
28483
|
+
await reply(`Agent "${agentId}" is already bound to Feishu user ${existingAgentBinding.externalUserId}. Unbind first if you want to rebind.`);
|
|
28484
|
+
return;
|
|
28485
|
+
}
|
|
28486
|
+
try {
|
|
28487
|
+
await createAgentMapping(db, {
|
|
28488
|
+
platform: "feishu",
|
|
28489
|
+
externalUserId: event.senderId,
|
|
28490
|
+
agentId,
|
|
28491
|
+
boundVia: "command",
|
|
28492
|
+
displayName: void 0
|
|
28493
|
+
});
|
|
28494
|
+
} catch (err) {
|
|
28495
|
+
log.error({
|
|
28496
|
+
err,
|
|
28497
|
+
agentId,
|
|
28498
|
+
senderId: event.senderId
|
|
28499
|
+
}, "/bind: failed to create mapping");
|
|
28500
|
+
await reply("Binding failed due to an internal error. Please try again or contact your admin.");
|
|
28501
|
+
return;
|
|
28502
|
+
}
|
|
28503
|
+
await reply(`Binding successful! Your Feishu account is now linked to "${agentId}".`);
|
|
28504
|
+
log.info({
|
|
28505
|
+
agentId,
|
|
28506
|
+
senderId: event.senderId,
|
|
28507
|
+
appId: bot.appId
|
|
28508
|
+
}, "/bind: Feishu user bound via command");
|
|
28509
|
+
}
|
|
28152
28510
|
/**
|
|
28153
28511
|
* Process outbound messages for all feishu-bound human agents.
|
|
28154
28512
|
* Consumes pending inbox entries for human agents that have feishu platform bindings,
|
|
@@ -28343,6 +28701,7 @@ async function buildApp(config) {
|
|
|
28343
28701
|
});
|
|
28344
28702
|
const agentAuth = agentAuthHook(db);
|
|
28345
28703
|
const adminAuth = adminAuthHook(db, config.secrets.jwtSecret);
|
|
28704
|
+
const githubAuth = githubAuthHook();
|
|
28346
28705
|
app.setErrorHandler((error, _request, reply) => {
|
|
28347
28706
|
if (error instanceof AppError) return reply.status(error.statusCode).send({ error: error.message });
|
|
28348
28707
|
if (error instanceof ZodError) return reply.status(400).send({
|
|
@@ -28357,6 +28716,11 @@ async function buildApp(config) {
|
|
|
28357
28716
|
await api.register(healthRoutes);
|
|
28358
28717
|
await api.register(githubWebhookRoutes, { prefix: "/webhooks" });
|
|
28359
28718
|
await api.register(adminAuthRoutes, { prefix: "/admin/auth" });
|
|
28719
|
+
await api.register(contextTreeInfoRoutes, { prefix: "/context-tree" });
|
|
28720
|
+
await api.register(async (bootstrapApp) => {
|
|
28721
|
+
bootstrapApp.addHook("onRequest", githubAuth);
|
|
28722
|
+
await bootstrapApp.register(bootstrapRoutes);
|
|
28723
|
+
}, { prefix: "/bootstrap" });
|
|
28360
28724
|
await api.register(async (adminApp) => {
|
|
28361
28725
|
adminApp.addHook("onRequest", adminAuth);
|
|
28362
28726
|
await adminApp.register(adminAgentRoutes);
|
|
@@ -28401,6 +28765,8 @@ async function buildApp(config) {
|
|
|
28401
28765
|
await agentApp.register(agentSendToAgentRoutes, { prefix: "/agents" });
|
|
28402
28766
|
await agentApp.register(agentInboxRoutes, { prefix: "/inbox" });
|
|
28403
28767
|
await agentApp.register(agentContextTreeRoutes, { prefix: "/context-tree" });
|
|
28768
|
+
await agentApp.register(agentFeishuBotRoutes);
|
|
28769
|
+
await agentApp.register(agentFeishuUserRoutes, { prefix: "/delegated" });
|
|
28404
28770
|
await agentApp.register(agentWsRoutes(notifier, config.instanceId), { prefix: "/ws" });
|
|
28405
28771
|
}, { prefix: "/agent" });
|
|
28406
28772
|
}, { prefix: "/api/v1" });
|
|
@@ -28560,4 +28926,4 @@ function resolveWebDist() {
|
|
|
28560
28926
|
} catch {}
|
|
28561
28927
|
}
|
|
28562
28928
|
//#endregion
|
|
28563
|
-
export {
|
|
28929
|
+
export { ClientRuntime as A, registerBuiltinHandlers as B, checkWebSocket as C, ensurePostgres as D, status as E, SdkError as F, hasAdminUser as H, SessionRegistry as I, cleanWorkspaces as L, AgentSlot as M, DEFAULT_WORKSPACE_TTL_MS as N, isDockerAvailable as O, FirstTreeHubSDK as P, getHandlerFactory as R, checkServerReachable as S, blank as T, createAdminUser$1 as V, checkDocker as _, formatCheckReport as a, checkServerConfig as b, onboardContinue as c, runMigrations as d, checkAgentConfigs as f, checkDatabase as g, checkContextTreeRepo as h, promptMissingFields as i, AgentRuntime as j, stopPostgres as k, onboardCreate as l, checkClientConfig as m, isInteractive as n, loadOnboardState as o, checkAgentTokens as p, promptAddAgent as r, onboardCheck as s, startServer as t, saveOnboardState as u, checkGitHubToken as v, printResults as w, checkServerHealth as x, checkNodeVersion as y, loadRuntimeConfig as z };
|