@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.
@@ -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, stringify } from "yaml";
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-DtKgrri9.mjs
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 existing = await db.execute(sql`SELECT id, status, type, display_name, delegate_mention, tree_path FROM agents WHERE id = ${member.name}`);
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
- if (agent.status === "suspended") {
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
- "Please contact your admin to set up the binding.",
28134
- `Your user ID: ${event.senderId}`
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 { SessionRegistry as A, clientConfigSchema as B, stopPostgres as C, DEFAULT_WORKSPACE_TTL_MS as D, AgentSlot as E, createAdminUser$1 as F, resetConfig as G, initConfig as H, hasAdminUser as I, setConfigValue as J, resetConfigMeta as K, DEFAULT_CONFIG_DIR as L, getHandlerFactory as M, loadRuntimeConfig as N, FirstTreeHubSDK as O, registerBuiltinHandlers as P, DEFAULT_DATA_DIR$1 as R, isDockerAvailable as S, AgentRuntime as T, loadAgents as U, getConfigValue as V, readConfigFile as W, checkWebSocket as _, runMigrations as a, status as b, checkClientConfig as c, checkDocker as d, checkGitHubToken as f, checkServerReachable as g, checkServerHealth as h, promptMissingFields as i, cleanWorkspaces as j, SdkError as k, checkContextTreeRepo as l, checkServerConfig as m, isInteractive as n, checkAgentConfigs as o, checkNodeVersion as p, serverConfigSchema as q, promptAddAgent as r, checkAgentTokens as s, startServer as t, checkDatabase as u, printResults as v, ClientRuntime as w, ensurePostgres as x, blank as y, agentConfigSchema as z };
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 };