@agent-team-foundation/first-tree-hub 0.3.3 → 0.3.5

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,18 +1,18 @@
1
- import { a as getGitHubUsername, b as serverConfigSchema, c as DEFAULT_CONFIG_DIR, d as clientConfigSchema, f as collectMissingPrompts, h as loadAgents, m as initConfig, r as checkBootstrapStatus, s as resolveServerUrl, t as bootstrapToken$1, u as agentConfigSchema, x as setConfigValue, y as resolveConfigReadonly } from "./bootstrap-waEJYq3g.mjs";
2
- import { ZodError, z } from "zod";
1
+ import { S as setConfigValue, a as getGitHubUsername, b as resolveConfigReadonly, c as DEFAULT_CONFIG_DIR, d as agentConfigSchema, f as clientConfigSchema, g as loadAgents, h as initConfig, p as collectMissingPrompts, r as checkBootstrapStatus, s as resolveServerUrl, t as bootstrapToken$1, u as DEFAULT_HOME_DIR$1, x as serverConfigSchema } from "./bootstrap-mhkpeOEc.mjs";
3
2
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
3
  import { dirname, join, resolve } from "node:path";
5
- import { parse } from "yaml";
4
+ import { EventEmitter } from "node:events";
5
+ import WebSocket from "ws";
6
6
  import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
7
+ import { query } from "@anthropic-ai/claude-agent-sdk";
8
+ import { execFileSync, execSync } from "node:child_process";
9
+ import { ZodError, z } from "zod";
10
+ import "yaml";
7
11
  import { homedir } from "node:os";
8
12
  import bcrypt from "bcrypt";
9
13
  import { and, desc, eq, gt, inArray, isNotNull, isNull, lt, ne, sql } from "drizzle-orm";
10
14
  import { drizzle } from "drizzle-orm/postgres-js";
11
15
  import postgres from "postgres";
12
- import { execFileSync, execSync } from "node:child_process";
13
- import { EventEmitter } from "node:events";
14
- import WebSocket from "ws";
15
- import { query } from "@anthropic-ai/claude-agent-sdk";
16
16
  import { fileURLToPath } from "node:url";
17
17
  import { migrate } from "drizzle-orm/postgres-js/migrator";
18
18
  import { input, password, select } from "@inquirer/prompts";
@@ -24,40 +24,6 @@ import Fastify from "fastify";
24
24
  import { bigserial, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique } from "drizzle-orm/pg-core";
25
25
  import { SignJWT, jwtVerify } from "jose";
26
26
  import { Client, EventDispatcher, LoggerLevel, WSClient } from "@larksuiteoapi/node-sdk";
27
- //#region src/core/admin.ts
28
- /**
29
- * Check if any admin user exists.
30
- */
31
- async function hasAdminUser(databaseUrl) {
32
- const client = postgres(databaseUrl, { max: 1 });
33
- try {
34
- return (await client`SELECT count(*)::int AS count FROM admin_users`)[0].count > 0;
35
- } finally {
36
- await client.end();
37
- }
38
- }
39
- /**
40
- * Create an admin user. Returns the generated password.
41
- */
42
- async function createAdminUser$1(databaseUrl, username, password) {
43
- const pw = password ?? randomBytes(12).toString("base64url");
44
- const hash = await bcrypt.hash(pw, 12);
45
- const client = postgres(databaseUrl, { max: 1 });
46
- const db = drizzle(client);
47
- try {
48
- await db.execute(sql`
49
- INSERT INTO admin_users (id, username, password_hash, role)
50
- VALUES (${randomUUID()}, ${username}, ${hash}, 'super_admin')
51
- `);
52
- } finally {
53
- await client.end();
54
- }
55
- return {
56
- username,
57
- password: pw
58
- };
59
- }
60
- //#endregion
61
27
  //#region ../client/dist/index.mjs
62
28
  const FETCH_TIMEOUT_MS = 15e3;
63
29
  var FirstTreeHubSDK = class {
@@ -429,7 +395,7 @@ defineConfig({
429
395
  "error"
430
396
  ]).default("info"), { env: "FIRST_TREE_HUB_LOG_LEVEL" })
431
397
  });
432
- const DEFAULT_HOME_DIR = join(homedir(), ".first-tree-hub");
398
+ const DEFAULT_HOME_DIR = process.env.FIRST_TREE_HUB_HOME ?? join(homedir(), ".first-tree-hub");
433
399
  join(DEFAULT_HOME_DIR, "config");
434
400
  const DEFAULT_DATA_DIR = join(DEFAULT_HOME_DIR, "data");
435
401
  defineConfig({
@@ -494,111 +460,17 @@ defineConfig({
494
460
  max: field(z.number().default(100), { env: "FIRST_TREE_HUB_RATE_LIMIT_MAX" }),
495
461
  loginMax: field(z.number().default(5), { env: "FIRST_TREE_HUB_RATE_LIMIT_LOGIN_MAX" }),
496
462
  webhookMax: field(z.number().default(60), { env: "FIRST_TREE_HUB_RATE_LIMIT_WEBHOOK_MAX" })
463
+ }),
464
+ kael: optional({
465
+ endpoint: field(z.string(), { env: "KAEL_ENDPOINT" }),
466
+ apiKey: field(z.string(), {
467
+ env: "KAEL_API_KEY",
468
+ secret: true
469
+ }),
470
+ hubPublicUrl: field(z.string(), { env: "FIRST_TREE_HUB_PUBLIC_URL" })
497
471
  })
498
472
  });
499
- const CONTEXT_TREE_DIR$1 = join(DEFAULT_DATA_DIR, "context-tree");
500
- /**
501
- * Sync the shared Context Tree git clone.
502
- *
503
- * Clones on first run, pulls on subsequent runs.
504
- * Returns the clone path on success, null on failure (graceful degradation).
505
- */
506
- async function syncContextTree(serverUrl, token, log) {
507
- try {
508
- execFileSync("git", ["--version"], { stdio: "ignore" });
509
- } catch {
510
- log("Context Tree sync skipped: git is not installed");
511
- return null;
512
- }
513
- let repo;
514
- let branch;
515
- try {
516
- const config = await new FirstTreeHubSDK({
517
- serverUrl,
518
- token
519
- }).getContextTreeConfig();
520
- repo = config.repo;
521
- branch = config.branch;
522
- } catch (err) {
523
- log(`Context Tree sync skipped: failed to fetch config from server (${err instanceof Error ? err.message : String(err)})`);
524
- return null;
525
- }
526
- try {
527
- if (existsSync(join(CONTEXT_TREE_DIR$1, ".git"))) {
528
- if (execFileSync("git", [
529
- "rev-parse",
530
- "--abbrev-ref",
531
- "HEAD"
532
- ], {
533
- cwd: CONTEXT_TREE_DIR$1,
534
- encoding: "utf-8",
535
- timeout: 5e3
536
- }).trim() !== branch) {
537
- execFileSync("git", ["checkout", branch], {
538
- cwd: CONTEXT_TREE_DIR$1,
539
- stdio: "pipe",
540
- timeout: 1e4
541
- });
542
- log(`Context Tree switched to branch ${branch}`);
543
- }
544
- execFileSync("git", ["pull", "--ff-only"], {
545
- cwd: CONTEXT_TREE_DIR$1,
546
- stdio: "pipe",
547
- timeout: 3e4
548
- });
549
- log(`Context Tree updated (pull)`);
550
- } else {
551
- mkdirSync(CONTEXT_TREE_DIR$1, { recursive: true });
552
- execFileSync("git", [
553
- "clone",
554
- "--branch",
555
- branch,
556
- "--single-branch",
557
- repo,
558
- CONTEXT_TREE_DIR$1
559
- ], {
560
- stdio: "pipe",
561
- timeout: 6e4
562
- });
563
- log(`Context Tree cloned from ${repo} (branch: ${branch})`);
564
- }
565
- return CONTEXT_TREE_DIR$1;
566
- } catch (err) {
567
- const msg = err instanceof Error ? err.message : String(err);
568
- log(`Context Tree sync failed: ${msg}`);
569
- log("Check that git credentials (SSH key or credential helper) are configured for this repo");
570
- if ((msg.includes("cannot fast-forward") || msg.includes("not possible to fast-forward") || msg.includes("CONFLICT")) && existsSync(join(CONTEXT_TREE_DIR$1, ".git"))) {
571
- log("Diverged history detected, attempting fresh clone...");
572
- try {
573
- rmSync(CONTEXT_TREE_DIR$1, {
574
- recursive: true,
575
- force: true
576
- });
577
- mkdirSync(CONTEXT_TREE_DIR$1, { recursive: true });
578
- execFileSync("git", [
579
- "clone",
580
- "--branch",
581
- branch,
582
- "--single-branch",
583
- repo,
584
- CONTEXT_TREE_DIR$1
585
- ], {
586
- stdio: "pipe",
587
- timeout: 6e4
588
- });
589
- log("Context Tree re-cloned successfully");
590
- return CONTEXT_TREE_DIR$1;
591
- } catch {
592
- log("Context Tree re-clone also failed, continuing without context");
593
- }
594
- }
595
- if (existsSync(join(CONTEXT_TREE_DIR$1, ".git"))) {
596
- log("Using existing Context Tree clone despite sync failure");
597
- return CONTEXT_TREE_DIR$1;
598
- }
599
- return null;
600
- }
601
- }
473
+ join(DEFAULT_DATA_DIR, "context-tree");
602
474
  /**
603
475
  * Bootstrap a workspace with .agent/ directory files.
604
476
  *
@@ -1454,92 +1326,44 @@ const agentSlotConfigSchema = z.object({
1454
1326
  session: sessionConfigSchema.prefault({}),
1455
1327
  concurrency: z.number().int().positive().default(5)
1456
1328
  });
1457
- const runtimeConfigSchema = z.object({
1329
+ z.object({
1458
1330
  server: z.url().default("http://localhost:8000"),
1459
1331
  agents: z.record(z.string(), agentSlotConfigSchema).refine((agents) => Object.keys(agents).length > 0, "At least one agent must be defined")
1460
1332
  });
1461
- function expandEnvVars(value) {
1462
- return value.replace(/\$\{([^}]+)\}/g, (_, name) => {
1463
- const envVal = process.env[name];
1464
- if (envVal === void 0) throw new Error(`Environment variable "${name}" is not set`);
1465
- return envVal;
1466
- });
1467
- }
1468
- function deepExpandEnv(obj) {
1469
- if (typeof obj === "string") return expandEnvVars(obj);
1470
- if (Array.isArray(obj)) return obj.map(deepExpandEnv);
1471
- if (obj !== null && typeof obj === "object") {
1472
- const result = {};
1473
- for (const [key, value] of Object.entries(obj)) result[key] = deepExpandEnv(value);
1474
- return result;
1333
+ //#endregion
1334
+ //#region src/core/admin.ts
1335
+ /**
1336
+ * Check if any admin user exists.
1337
+ */
1338
+ async function hasAdminUser(databaseUrl) {
1339
+ const client = postgres(databaseUrl, { max: 1 });
1340
+ try {
1341
+ return (await client`SELECT count(*)::int AS count FROM admin_users`)[0].count > 0;
1342
+ } finally {
1343
+ await client.end();
1475
1344
  }
1476
- return obj;
1477
1345
  }
1478
- function loadRuntimeConfig(configPath) {
1479
- const expanded = deepExpandEnv(parse(readFileSync(configPath, "utf-8")));
1480
- return runtimeConfigSchema.parse(expanded);
1481
- }
1482
- const DEFAULT_SHUTDOWN_TIMEOUT = 3e4;
1483
- var AgentRuntime = class {
1484
- slots = [];
1485
- config;
1486
- shutdownTimeout;
1487
- stopping = false;
1488
- constructor(options) {
1489
- this.config = options.config;
1490
- this.shutdownTimeout = options.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT;
1491
- for (const [name, agentConfig] of Object.entries(this.config.agents)) {
1492
- const handlerFactory = getHandlerFactory(agentConfig.type);
1493
- this.slots.push(new AgentSlot({
1494
- name,
1495
- serverUrl: this.config.server,
1496
- token: agentConfig.token,
1497
- type: agentConfig.type,
1498
- handlerFactory,
1499
- session: agentConfig.session,
1500
- concurrency: agentConfig.concurrency
1501
- }));
1502
- }
1503
- }
1504
- /** Start all agent slots and block until shutdown signal. */
1505
- async start() {
1506
- const log = (msg) => process.stderr.write(`[runtime] ${msg}\n`);
1507
- const firstToken = Object.values(this.config.agents)[0]?.token;
1508
- let contextTreePath = null;
1509
- if (firstToken) contextTreePath = await syncContextTree(this.config.server, firstToken, log);
1510
- if (!contextTreePath) log("WARNING: Context Tree sync failed — agents will start without organizational context");
1511
- log(`Starting ${this.slots.length} agent(s)...`);
1512
- const results = await Promise.allSettled(this.slots.map((slot) => slot.start(contextTreePath)));
1513
- let failed = 0;
1514
- for (const result of results) if (result.status === "rejected") {
1515
- log(`Failed to start agent: ${result.reason instanceof Error ? result.reason.message : result.reason}`);
1516
- failed++;
1517
- }
1518
- if (failed === this.slots.length) throw new Error("All agents failed to start");
1519
- log("Ready. Press Ctrl+C to stop.");
1520
- await new Promise((resolve) => {
1521
- const shutdown = async () => {
1522
- if (this.stopping) return;
1523
- this.stopping = true;
1524
- log("Shutting down...");
1525
- const timer = setTimeout(() => {
1526
- log("Shutdown timeout reached, forcing exit");
1527
- process.exit(1);
1528
- }, this.shutdownTimeout);
1529
- await this.stop();
1530
- clearTimeout(timer);
1531
- log("Stopped");
1532
- resolve();
1533
- };
1534
- process.on("SIGINT", shutdown);
1535
- process.on("SIGTERM", shutdown);
1536
- });
1537
- }
1538
- /** Stop all slots. */
1539
- async stop() {
1540
- await Promise.allSettled(this.slots.map((slot) => slot.stop()));
1346
+ /**
1347
+ * Create an admin user. Returns the generated password.
1348
+ */
1349
+ async function createAdminUser$1(databaseUrl, username, password) {
1350
+ const pw = password ?? randomBytes(12).toString("base64url");
1351
+ const hash = await bcrypt.hash(pw, 12);
1352
+ const client = postgres(databaseUrl, { max: 1 });
1353
+ const db = drizzle(client);
1354
+ try {
1355
+ await db.execute(sql`
1356
+ INSERT INTO admin_users (id, username, password_hash, role)
1357
+ VALUES (${randomUUID()}, ${username}, ${hash}, 'super_admin')
1358
+ `);
1359
+ } finally {
1360
+ await client.end();
1541
1361
  }
1542
- };
1362
+ return {
1363
+ username,
1364
+ password: pw
1365
+ };
1366
+ }
1543
1367
  //#endregion
1544
1368
  //#region src/core/client-runtime.ts
1545
1369
  /**
@@ -2187,10 +2011,10 @@ async function runMigrations(databaseUrl) {
2187
2011
  }
2188
2012
  //#endregion
2189
2013
  //#region src/core/onboard.ts
2190
- const STATE_FILE = join(homedir(), ".first-tree-hub", ".onboard-state.json");
2014
+ const STATE_FILE = join(DEFAULT_HOME_DIR$1, ".onboard-state.json");
2191
2015
  /** Save current onboard args to state file for resume. */
2192
2016
  function saveOnboardState(args) {
2193
- mkdirSync(join(homedir(), ".first-tree-hub"), { recursive: true });
2017
+ mkdirSync(DEFAULT_HOME_DIR$1, { recursive: true });
2194
2018
  writeFileSync(STATE_FILE, JSON.stringify({ args }, null, 2));
2195
2019
  }
2196
2020
  /** Load saved onboard args from state file. */
@@ -2323,7 +2147,7 @@ async function onboardCheck(args) {
2323
2147
  status: "missing_optional",
2324
2148
  hint: `defaults to "${args.id ?? ""}"`
2325
2149
  });
2326
- items.push(args.assistant ? {
2150
+ if (args.type === "human") items.push(args.assistant ? {
2327
2151
  key: "assistant",
2328
2152
  label: "assistant",
2329
2153
  status: "ok",
@@ -2334,7 +2158,7 @@ async function onboardCheck(args) {
2334
2158
  status: "missing_optional",
2335
2159
  hint: "Also create a personal_assistant"
2336
2160
  });
2337
- items.push(args.feishuBotAppId ? {
2161
+ if (args.type !== "human" || args.assistant) items.push(args.feishuBotAppId ? {
2338
2162
  key: "feishu_bot",
2339
2163
  label: "feishu-bot-app-id",
2340
2164
  status: "ok",
@@ -2343,7 +2167,7 @@ async function onboardCheck(args) {
2343
2167
  key: "feishu_bot",
2344
2168
  label: "feishu-bot-app-id",
2345
2169
  status: "missing_optional",
2346
- hint: "Feishu bot App ID for assistant"
2170
+ hint: "Feishu bot App ID"
2347
2171
  });
2348
2172
  if (args.id && repoPath) if (existsSync(join(repoPath, "members", args.id))) try {
2349
2173
  execSync(`git ls-files --error-unmatch members/${args.id}/NODE.md`, {
@@ -2385,6 +2209,7 @@ function formatCheckReport(items) {
2385
2209
  async function onboardCreate(args) {
2386
2210
  const repoPath = await resolveContextTreeRepo(args.server);
2387
2211
  if (!repoPath) throw new Error("Context Tree repo not available. Ensure --server is configured and the server is running.");
2212
+ if (args.assistant && args.type !== "human") throw new Error(`--assistant is only valid for human agents, not ${args.type}`);
2388
2213
  const ghUsername = getGitHubUsername();
2389
2214
  const githubField = args.type === "human" ? ghUsername : null;
2390
2215
  const humanNodePath = join(repoPath, "members", args.id, "NODE.md");
@@ -2507,7 +2332,7 @@ async function onboardCreate(args) {
2507
2332
  branch,
2508
2333
  prUrl: prOutput
2509
2334
  };
2510
- mkdirSync(join(homedir(), ".first-tree-hub"), { recursive: true });
2335
+ mkdirSync(DEFAULT_HOME_DIR$1, { recursive: true });
2511
2336
  writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
2512
2337
  return { prUrl: prOutput };
2513
2338
  }
@@ -2550,7 +2375,7 @@ async function onboardContinue(args) {
2550
2375
  first-tree-hub onboard --continue`);
2551
2376
  throw err;
2552
2377
  }
2553
- process.stderr.write(`Token saved to ~/.first-tree-hub/agents/${agentToBootstrap}/agent.yaml\n`);
2378
+ process.stderr.write(`Token saved to ${DEFAULT_HOME_DIR$1}/config/agents/${agentToBootstrap}/agent.yaml\n`);
2554
2379
  if (mergedArgs.feishuBotAppId && mergedArgs.feishuBotAppSecret) {
2555
2380
  const { bindFeishuBot } = await import("./feishu-Y4m2zFc3.mjs").then((n) => n.r);
2556
2381
  process.stderr.write("Binding Feishu bot...\n");
@@ -2561,16 +2386,20 @@ async function onboardContinue(args) {
2561
2386
  const { unlinkSync } = await import("node:fs");
2562
2387
  unlinkSync(STATE_FILE);
2563
2388
  } catch {}
2389
+ const typeLabel = mergedArgs.type === "human" ? "Human" : mergedArgs.type === "autonomous_agent" ? "Agent" : "Assistant";
2564
2390
  process.stderr.write("\n✅ Onboard complete!\n\n");
2565
- process.stderr.write(` Human: ${mergedArgs.id}\n`);
2391
+ process.stderr.write(` ${typeLabel}:${" ".repeat(Math.max(1, 10 - typeLabel.length))}${mergedArgs.id}\n`);
2566
2392
  if (mergedArgs.assistant) process.stderr.write(` Assistant: ${mergedArgs.assistant}\n`);
2567
- process.stderr.write(` Token: ~/.first-tree-hub/agents/${agentToBootstrap}/agent.yaml\n`);
2393
+ process.stderr.write(` Token: ${DEFAULT_HOME_DIR$1}/config/agents/${agentToBootstrap}/agent.yaml\n`);
2568
2394
  if (mergedArgs.feishuBotAppId) process.stderr.write(` Feishu: bot bound (${mergedArgs.feishuBotAppId})\n`);
2395
+ setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", serverUrl);
2569
2396
  if (mergedArgs.type === "human") {
2570
2397
  process.stderr.write("\n Next step — bind your Feishu account:\n");
2571
2398
  process.stderr.write(` Send this message to the bot in Feishu: /bind ${mergedArgs.id}\n`);
2572
2399
  if (!mergedArgs.feishuBotAppId) process.stderr.write(" (requires a Feishu bot to be configured in the system)\n");
2573
2400
  }
2401
+ process.stderr.write("\n Start the agent:\n");
2402
+ process.stderr.write(" first-tree-hub client start\n");
2574
2403
  process.stderr.write("\n");
2575
2404
  }
2576
2405
  function createMemberNodeMd(repoPath, data) {
@@ -2578,7 +2407,16 @@ function createMemberNodeMd(repoPath, data) {
2578
2407
  mkdirSync(memberDir, { recursive: true });
2579
2408
  const domainsList = data.domains.map((d) => ` - "${d}"`).join("\n");
2580
2409
  const githubLine = data.github ? `\ngithub: ${data.github}` : "";
2581
- const delegateLine = data.delegateMention ? `\ndelegate_mention: ${data.delegateMention}` : "";
2410
+ const delegateLine = data.delegateMention && data.type === "human" ? `\ndelegate_mention: ${data.delegateMention}` : "";
2411
+ const bodySections = data.type === "autonomous_agent" ? `## About
2412
+
2413
+ ## Capabilities
2414
+
2415
+ ## Current Focus
2416
+ ` : `## About
2417
+
2418
+ ## Current Focus
2419
+ `;
2582
2420
  const content = `---
2583
2421
  title: "${data.displayName}"
2584
2422
  owners: [${data.owner}]
@@ -2590,10 +2428,7 @@ ${domainsList}${githubLine}${delegateLine}
2590
2428
 
2591
2429
  # ${data.displayName}
2592
2430
 
2593
- ## About
2594
-
2595
- ## Current Focus
2596
- `;
2431
+ ${bodySections}`;
2597
2432
  writeFileSync(join(memberDir, "NODE.md"), content);
2598
2433
  }
2599
2434
  function isTrackedByGit(repoPath, filePath) {
@@ -2607,9 +2442,9 @@ function isTrackedByGit(repoPath, filePath) {
2607
2442
  return false;
2608
2443
  }
2609
2444
  }
2610
- const CONTEXT_TREE_DIR = join(homedir(), ".first-tree-hub", "context-tree");
2445
+ const CONTEXT_TREE_DIR = join(DEFAULT_HOME_DIR$1, "context-tree");
2611
2446
  /**
2612
- * Resolve Context Tree to a **local path** at ~/.first-tree-hub/context-tree/.
2447
+ * Resolve Context Tree to a **local path** at $FIRST_TREE_HUB_HOME/context-tree/.
2613
2448
  *
2614
2449
  * Repo URL is obtained from the Hub server. The local clone is always
2615
2450
  * managed in the standard location — no custom paths allowed.
@@ -2656,13 +2491,13 @@ async function resolveContextTreeRepo(serverUrl) {
2656
2491
  return CONTEXT_TREE_DIR;
2657
2492
  }
2658
2493
  } catch {}
2659
- const safePrefix = join(homedir(), ".first-tree-hub");
2494
+ const safePrefix = DEFAULT_HOME_DIR$1;
2660
2495
  if (!CONTEXT_TREE_DIR.startsWith(safePrefix) || CONTEXT_TREE_DIR === safePrefix) throw new Error(`Refusing to delete unsafe path: ${CONTEXT_TREE_DIR}`);
2661
2496
  execSync(`rm -rf ${CONTEXT_TREE_DIR}`);
2662
2497
  }
2663
2498
  try {
2664
2499
  process.stderr.write(`Cloning Context Tree to ${CONTEXT_TREE_DIR}...\n`);
2665
- mkdirSync(join(homedir(), ".first-tree-hub"), { recursive: true });
2500
+ mkdirSync(DEFAULT_HOME_DIR$1, { recursive: true });
2666
2501
  execSync(`git ${gitConfigArgs} clone ${repoUrl} ${CONTEXT_TREE_DIR}`, {
2667
2502
  stdio: "pipe",
2668
2503
  env: gitEnv
@@ -2727,8 +2562,7 @@ async function promptMissingFields(options) {
2727
2562
  const envStr = envHint ? ` (env: ${envHint})` : "";
2728
2563
  return ` ${m.dotPath}${envStr}`;
2729
2564
  });
2730
- throw new Error(`Missing required configuration:\n${lines.join("\n")}\n\nProvide values via environment variables, config file (~/.first-tree-hub/server.yaml),
2731
- or run without --no-interactive to use the interactive setup wizard.`);
2565
+ throw new Error(`Missing required configuration:\n${lines.join("\n")}\n\nProvide values via environment variables, config file (${DEFAULT_HOME_DIR$1}/server.yaml),\nor run without --no-interactive to use the interactive setup wizard.`);
2732
2566
  }
2733
2567
  const configPath = join(options.configDir ?? DEFAULT_CONFIG_DIR, `${options.role}.yaml`);
2734
2568
  const results = {};
@@ -2808,7 +2642,11 @@ function setNestedByDot(obj, dotPath, value) {
2808
2642
  }
2809
2643
  //#endregion
2810
2644
  //#region ../shared/dist/index.mjs
2811
- const adapterPlatformSchema = z.enum(["feishu", "slack"]);
2645
+ const adapterPlatformSchema = z.enum([
2646
+ "feishu",
2647
+ "slack",
2648
+ "kael"
2649
+ ]);
2812
2650
  const adapterStatusSchema = z.enum(["active", "inactive"]);
2813
2651
  const createAdapterConfigSchema = z.object({
2814
2652
  platform: adapterPlatformSchema,
@@ -3081,7 +2919,7 @@ const SYSTEM_CONFIG_DEFAULTS = {
3081
2919
  [SYSTEM_CONFIG_KEYS.PRESENCE_CLEANUP_SECONDS]: 60
3082
2920
  };
3083
2921
  //#endregion
3084
- //#region ../server/dist/app-BVTDWxJE.mjs
2922
+ //#region ../server/dist/app-CurdzcN2.mjs
3085
2923
  var __defProp = Object.defineProperty;
3086
2924
  var __exportAll = (all, no_symbols) => {
3087
2925
  let target = {};
@@ -5013,7 +4851,7 @@ async function pollInbox(db, inboxId, limit) {
5013
4851
  });
5014
4852
  });
5015
4853
  }
5016
- async function ackEntry$1(db, entryId, inboxId) {
4854
+ async function ackEntry$2(db, entryId, inboxId) {
5017
4855
  const [entry] = await db.update(inboxEntries).set({
5018
4856
  status: "acked",
5019
4857
  ackedAt: /* @__PURE__ */ new Date()
@@ -5055,7 +4893,7 @@ async function agentInboxRoutes(app) {
5055
4893
  app.post("/:entryId/ack", async (request, reply) => {
5056
4894
  const identity = requireAgent(request);
5057
4895
  const entryId = Number(request.params.entryId);
5058
- await ackEntry$1(app.db, entryId, identity.inboxId);
4896
+ await ackEntry$2(app.db, entryId, identity.inboxId);
5059
4897
  return reply.status(204).send();
5060
4898
  });
5061
4899
  app.post("/:entryId/renew", async (request, reply) => {
@@ -5828,7 +5666,7 @@ async function withoutProxy(fn) {
5828
5666
  for (const [key, val] of Object.entries(saved)) process.env[key] = val;
5829
5667
  }
5830
5668
  }
5831
- const OUTBOUND_BATCH_SIZE = 10;
5669
+ const OUTBOUND_BATCH_SIZE$1 = 10;
5832
5670
  /** Wrap an SDK API call with proxy bypass if needed. */
5833
5671
  function botApiCall(bot, fn) {
5834
5672
  return bot.bypassProxy ? withoutProxy(fn) : fn();
@@ -6003,6 +5841,8 @@ function parseEventData(appId, data) {
6003
5841
  const sender = data.sender;
6004
5842
  const message = data.message;
6005
5843
  if (!sender?.sender_id?.open_id || !message) return null;
5844
+ if (!sender.sender_id.union_id) process.stderr.write(`[warn] Feishu event missing union_id for sender ${sender.sender_id.open_id}, falling back to open_id\n`);
5845
+ const resolvedSenderId = sender.sender_id.union_id ?? sender.sender_id.open_id;
6006
5846
  const eventId = data.event_id ?? `ws_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
6007
5847
  let parsedContent;
6008
5848
  try {
@@ -6019,7 +5859,8 @@ function parseEventData(appId, data) {
6019
5859
  eventId,
6020
5860
  platform: "feishu",
6021
5861
  appId,
6022
- senderId: sender.sender_id.open_id,
5862
+ senderId: resolvedSenderId,
5863
+ senderOpenId: sender.sender_id.open_id,
6023
5864
  senderType: sender.sender_type ?? "user",
6024
5865
  externalChannelId: message.chat_id ?? "",
6025
5866
  chatType: message.chat_type ?? "group",
@@ -6192,7 +6033,7 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
6192
6033
  JOIN adapter_agent_mappings aam ON a.id = aam.agent_id
6193
6034
  WHERE aam.platform = 'feishu' AND ie.status = 'pending'
6194
6035
  ORDER BY ie.created_at
6195
- LIMIT ${OUTBOUND_BATCH_SIZE}
6036
+ LIMIT ${OUTBOUND_BATCH_SIZE$1}
6196
6037
  FOR UPDATE SKIP LOCKED
6197
6038
  )
6198
6039
  RETURNING id, inbox_id, message_id, chat_id
@@ -6201,21 +6042,21 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
6201
6042
  for (const entry of claimed) try {
6202
6043
  const [msg] = await db.select().from(messages).where(eq(messages.id, entry.message_id)).limit(1);
6203
6044
  if (!msg) {
6204
- await ackEntry(db, entry.id);
6045
+ await ackEntry$1(db, entry.id);
6205
6046
  continue;
6206
6047
  }
6207
6048
  if (msg.metadata?.source === "feishu") {
6208
- await ackEntry(db, entry.id);
6049
+ await ackEntry$1(db, entry.id);
6209
6050
  continue;
6210
6051
  }
6211
6052
  const channelMapping = await findExternalChannelByChat(db, "feishu", entry.chat_id ?? msg.chatId);
6212
6053
  if (!channelMapping) {
6213
- await ackEntry(db, entry.id);
6054
+ await ackEntry$1(db, entry.id);
6214
6055
  continue;
6215
6056
  }
6216
6057
  const dedupKey = `${msg.id}:${channelMapping.externalChannelId}`;
6217
6058
  if (sentMessages.has(dedupKey)) {
6218
- await ackEntry(db, entry.id);
6059
+ await ackEntry$1(db, entry.id);
6219
6060
  continue;
6220
6061
  }
6221
6062
  const bot = findBotByAgentId(msg.senderId);
@@ -6224,7 +6065,7 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
6224
6065
  messageId: msg.id,
6225
6066
  senderId: msg.senderId
6226
6067
  }, "Outbound skip: sender has no feishu bot binding");
6227
- await ackEntry(db, entry.id);
6068
+ await ackEntry$1(db, entry.id);
6228
6069
  continue;
6229
6070
  }
6230
6071
  const { msgType, content } = formatForFeishu(msg.format, msg.content);
@@ -6244,7 +6085,7 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
6244
6085
  externalMessageId: externalMsgId,
6245
6086
  externalChannelId: channelMapping.externalChannelId
6246
6087
  });
6247
- await ackEntry(db, entry.id);
6088
+ await ackEntry$1(db, entry.id);
6248
6089
  bot.lastActiveAt = /* @__PURE__ */ new Date();
6249
6090
  sent++;
6250
6091
  } catch (err) {
@@ -6259,7 +6100,7 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
6259
6100
  errors: errorCount
6260
6101
  };
6261
6102
  }
6262
- async function ackEntry(db, entryId) {
6103
+ async function ackEntry$1(db, entryId) {
6263
6104
  await db.update(inboxEntries).set({
6264
6105
  status: "acked",
6265
6106
  ackedAt: /* @__PURE__ */ new Date()
@@ -6297,10 +6138,11 @@ function formatForFeishu(format, content) {
6297
6138
  content: JSON.stringify({ text })
6298
6139
  };
6299
6140
  }
6300
- function createBackgroundTasks(app, instanceId, adapterManager) {
6141
+ function createBackgroundTasks(app, instanceId, adapterManager, kaelRuntime) {
6301
6142
  let inboxTimer = null;
6302
6143
  let heartbeatTimer = null;
6303
6144
  let adapterOutboundTimer = null;
6145
+ let kaelOutboundTimer = null;
6304
6146
  return {
6305
6147
  start() {
6306
6148
  inboxTimer = setInterval(async () => {
@@ -6329,6 +6171,13 @@ function createBackgroundTasks(app, instanceId, adapterManager) {
6329
6171
  app.log.error(err, "Adapter outbound processing failed");
6330
6172
  }
6331
6173
  }, 5e3);
6174
+ if (kaelRuntime) kaelOutboundTimer = setInterval(async () => {
6175
+ try {
6176
+ await kaelRuntime.processOutbound();
6177
+ } catch (err) {
6178
+ app.log.error(err, "Kael outbound processing failed");
6179
+ }
6180
+ }, 5e3);
6332
6181
  heartbeatInstance(app.db, instanceId).catch((err) => {
6333
6182
  app.log.error(err, "Failed initial heartbeat");
6334
6183
  });
@@ -6346,9 +6195,195 @@ function createBackgroundTasks(app, instanceId, adapterManager) {
6346
6195
  clearInterval(adapterOutboundTimer);
6347
6196
  adapterOutboundTimer = null;
6348
6197
  }
6198
+ if (kaelOutboundTimer) {
6199
+ clearInterval(kaelOutboundTimer);
6200
+ kaelOutboundTimer = null;
6201
+ }
6202
+ }
6203
+ };
6204
+ }
6205
+ const OUTBOUND_BATCH_SIZE = 10;
6206
+ function createKaelRuntime(db, encryptionKey, kaelEndpoint, kaelApiKey, serverUrl, log) {
6207
+ const agentConfigs = /* @__PURE__ */ new Map();
6208
+ const inboxToConfig = /* @__PURE__ */ new Map();
6209
+ let aborted = false;
6210
+ return {
6211
+ async reload() {
6212
+ if (!encryptionKey) {
6213
+ log.warn("Encryption key not set — Kael runtime disabled");
6214
+ return;
6215
+ }
6216
+ if (!kaelEndpoint) {
6217
+ log.debug("KAEL_ENDPOINT not configured — Kael runtime idle");
6218
+ agentConfigs.clear();
6219
+ inboxToConfig.clear();
6220
+ return;
6221
+ }
6222
+ const configs = await db.select().from(adapterConfigs).where(and(eq(adapterConfigs.platform, "kael"), eq(adapterConfigs.status, "active")));
6223
+ const configAgentIds = configs.filter((c) => c.credentials).map((c) => c.agentId);
6224
+ const agentRows = configAgentIds.length > 0 ? await db.execute(sql`SELECT id, inbox_id FROM agents WHERE id IN (${sql.join(configAgentIds.map((id) => sql`${id}`), sql`, `)}) AND status = 'active'`) : [];
6225
+ const agentInboxMap = new Map(agentRows.map((a) => [a.id, a.inbox_id]));
6226
+ const seen = /* @__PURE__ */ new Set();
6227
+ for (const config of configs) {
6228
+ if (!config.credentials) continue;
6229
+ let creds;
6230
+ try {
6231
+ creds = decryptCredentials(config.credentials, encryptionKey);
6232
+ } catch (err) {
6233
+ log.error({
6234
+ configId: config.id,
6235
+ err
6236
+ }, "Failed to decrypt Kael adapter credentials");
6237
+ continue;
6238
+ }
6239
+ seen.add(config.agentId);
6240
+ const inboxId = agentInboxMap.get(config.agentId);
6241
+ if (!inboxId) {
6242
+ log.warn({
6243
+ configId: config.id,
6244
+ agentId: config.agentId
6245
+ }, "Kael config agent not found or inactive");
6246
+ continue;
6247
+ }
6248
+ const entry = {
6249
+ configId: config.id,
6250
+ agentId: config.agentId,
6251
+ inboxId,
6252
+ kaelUserId: creds.kaelUserId,
6253
+ kaelProjectId: creds.kaelProjectId,
6254
+ agentToken: creds.agentToken
6255
+ };
6256
+ agentConfigs.set(config.agentId, entry);
6257
+ inboxToConfig.set(inboxId, entry);
6258
+ log.info({
6259
+ configId: config.id,
6260
+ agentId: config.agentId
6261
+ }, "Loaded Kael adapter config");
6262
+ }
6263
+ for (const agentId of agentConfigs.keys()) if (!seen.has(agentId)) {
6264
+ const old = agentConfigs.get(agentId);
6265
+ if (old) inboxToConfig.delete(old.inboxId);
6266
+ agentConfigs.delete(agentId);
6267
+ log.info({ agentId }, "Removed inactive Kael adapter config");
6268
+ }
6269
+ },
6270
+ async processOutbound() {
6271
+ if (agentConfigs.size === 0 || !kaelEndpoint || aborted) return {
6272
+ sent: 0,
6273
+ errors: 0
6274
+ };
6275
+ let sent = 0;
6276
+ let errorCount = 0;
6277
+ try {
6278
+ const agentIds = [...agentConfigs.keys()];
6279
+ const claimed = await db.execute(sql`
6280
+ UPDATE inbox_entries
6281
+ SET status = 'delivered', delivered_at = NOW()
6282
+ WHERE id IN (
6283
+ SELECT ie.id FROM inbox_entries ie
6284
+ JOIN agents a ON ie.inbox_id = a.inbox_id
6285
+ JOIN adapter_configs ac ON a.id = ac.agent_id
6286
+ WHERE ac.platform = 'kael' AND ac.status = 'active'
6287
+ AND ie.status = 'pending'
6288
+ AND a.id IN (${sql.join(agentIds.map((id) => sql`${id}`), sql`, `)})
6289
+ ORDER BY ie.created_at
6290
+ LIMIT ${OUTBOUND_BATCH_SIZE}
6291
+ FOR UPDATE OF ie SKIP LOCKED
6292
+ )
6293
+ RETURNING id, inbox_id, message_id, chat_id
6294
+ `);
6295
+ for (const entry of claimed) try {
6296
+ const [msg] = await db.select().from(messages).where(eq(messages.id, entry.message_id)).limit(1);
6297
+ if (!msg) {
6298
+ await ackEntry(db, entry.id);
6299
+ continue;
6300
+ }
6301
+ const config = inboxToConfig.get(entry.inbox_id);
6302
+ if (!config) {
6303
+ await ackEntry(db, entry.id);
6304
+ continue;
6305
+ }
6306
+ const messageContent = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
6307
+ const payload = {
6308
+ hub_chat_id: entry.chat_id ?? msg.chatId,
6309
+ hub_agent_id: config.agentId,
6310
+ hub_server_url: serverUrl,
6311
+ hub_agent_token: config.agentToken,
6312
+ user_id: config.kaelUserId,
6313
+ project_id: config.kaelProjectId,
6314
+ message: messageContent,
6315
+ sender_id: msg.senderId,
6316
+ format: msg.format
6317
+ };
6318
+ const response = await fetch(`${kaelEndpoint}/api/v1/hub/messages`, {
6319
+ method: "POST",
6320
+ headers: {
6321
+ "Content-Type": "application/json",
6322
+ ...kaelApiKey ? { "X-Internal-API-Key": kaelApiKey } : {}
6323
+ },
6324
+ body: JSON.stringify(payload)
6325
+ });
6326
+ if (!response.ok) {
6327
+ const body = await response.text().catch(() => "");
6328
+ log.error({
6329
+ entryId: entry.id,
6330
+ status: response.status,
6331
+ body
6332
+ }, "Kael API rejected outbound message");
6333
+ await nackEntry(db, entry.id);
6334
+ errorCount++;
6335
+ continue;
6336
+ }
6337
+ await ackEntry(db, entry.id);
6338
+ sent++;
6339
+ } catch (err) {
6340
+ log.error({
6341
+ entryId: entry.id,
6342
+ err
6343
+ }, "Failed to send outbound Kael message");
6344
+ await nackEntry(db, entry.id).catch((nackErr) => {
6345
+ log.error({
6346
+ entryId: entry.id,
6347
+ err: nackErr
6348
+ }, "Failed to NACK entry");
6349
+ });
6350
+ errorCount++;
6351
+ }
6352
+ } catch (err) {
6353
+ log.error({ err }, "Kael outbound processing error");
6354
+ return {
6355
+ sent: 0,
6356
+ errors: 1
6357
+ };
6358
+ }
6359
+ return {
6360
+ sent,
6361
+ errors: errorCount
6362
+ };
6363
+ },
6364
+ shutdown() {
6365
+ aborted = true;
6366
+ agentConfigs.clear();
6367
+ inboxToConfig.clear();
6349
6368
  }
6350
6369
  };
6351
6370
  }
6371
+ async function ackEntry(db, entryId) {
6372
+ await db.update(inboxEntries).set({
6373
+ status: "acked",
6374
+ ackedAt: /* @__PURE__ */ new Date()
6375
+ }).where(eq(inboxEntries.id, entryId));
6376
+ }
6377
+ const MAX_RETRY_COUNT = 3;
6378
+ async function nackEntry(db, entryId) {
6379
+ await db.execute(sql`
6380
+ UPDATE inbox_entries
6381
+ SET
6382
+ status = CASE WHEN retry_count >= ${MAX_RETRY_COUNT} THEN 'failed' ELSE 'pending' END,
6383
+ retry_count = retry_count + 1
6384
+ WHERE id = ${entryId}
6385
+ `);
6386
+ }
6352
6387
  async function buildApp(config) {
6353
6388
  const app = Fastify({ logger: config.logger ?? true });
6354
6389
  const db = connectDatabase(config.database.url);
@@ -6455,14 +6490,19 @@ async function buildApp(config) {
6455
6490
  app.decorate("notifier", notifier);
6456
6491
  const adapterManager = createAdapterManager(db, config.secrets.encryptionKey, app.log, notifier);
6457
6492
  app.decorate("adapterManager", adapterManager);
6458
- const backgroundTasks = createBackgroundTasks(app, config.instanceId, adapterManager);
6493
+ const kaelRuntime = config.kael?.endpoint ? createKaelRuntime(db, config.secrets.encryptionKey, config.kael.endpoint, config.kael.apiKey, config.kael.hubPublicUrl, app.log) : void 0;
6494
+ const backgroundTasks = createBackgroundTasks(app, config.instanceId, adapterManager, kaelRuntime);
6459
6495
  notifier.onConfigChange((configType) => {
6460
- if (configType === "adapter_configs") adapterManager.reload().catch((err) => app.log.error(err, "Adapter hot-reload failed (PG NOTIFY)"));
6496
+ if (configType === "adapter_configs") {
6497
+ adapterManager.reload().catch((err) => app.log.error(err, "Adapter hot-reload failed (PG NOTIFY)"));
6498
+ kaelRuntime?.reload().catch((err) => app.log.error(err, "Kael hot-reload failed (PG NOTIFY)"));
6499
+ }
6461
6500
  });
6462
6501
  app.addHook("onReady", async () => {
6463
6502
  await notifier.start();
6464
6503
  backgroundTasks.start();
6465
6504
  await adapterManager.reload();
6505
+ await kaelRuntime?.reload();
6466
6506
  const { repo: ctRepo, branch: ctBranch, syncInterval } = config.contextTree;
6467
6507
  const { token: ghToken } = config.github;
6468
6508
  try {
@@ -6480,6 +6520,7 @@ async function buildApp(config) {
6480
6520
  app.addHook("onClose", async () => {
6481
6521
  backgroundTasks.stop();
6482
6522
  adapterManager.shutdown();
6523
+ kaelRuntime?.shutdown();
6483
6524
  await notifier.stop();
6484
6525
  await listenClient.end();
6485
6526
  });
@@ -6594,4 +6635,4 @@ function resolveWebDist() {
6594
6635
  } catch {}
6595
6636
  }
6596
6637
  //#endregion
6597
- 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 };
6638
+ export { ClientRuntime as A, checkWebSocket as C, ensurePostgres as D, status as E, SessionRegistry as F, cleanWorkspaces as I, hasAdminUser as M, FirstTreeHubSDK as N, isDockerAvailable as O, SdkError as P, checkServerReachable as S, blank as T, checkDocker as _, formatCheckReport as a, checkServerConfig as b, onboardContinue as c, runMigrations as d, checkAgentConfigs as f, checkDatabase as g, checkContextTreeRepo as h, promptMissingFields as i, createAdminUser$1 as j, stopPostgres as k, onboardCreate as l, checkClientConfig as m, isInteractive as n, loadOnboardState as o, checkAgentTokens as p, promptAddAgent as r, onboardCheck as s, startServer as t, saveOnboardState as u, checkGitHubToken as v, printResults as w, checkServerHealth as x, checkNodeVersion as y };