@agent-team-foundation/first-tree-hub 0.3.4 → 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.
@@ -72,7 +72,7 @@ const clientConfigSchema = defineConfig({
72
72
  function getClientConfig() {
73
73
  return getConfig();
74
74
  }
75
- const DEFAULT_HOME_DIR = join(homedir(), ".first-tree-hub");
75
+ const DEFAULT_HOME_DIR = process.env.FIRST_TREE_HUB_HOME ?? join(homedir(), ".first-tree-hub");
76
76
  const DEFAULT_CONFIG_DIR = join(DEFAULT_HOME_DIR, "config");
77
77
  const DEFAULT_DATA_DIR = join(DEFAULT_HOME_DIR, "data");
78
78
  function isFieldDef(value) {
@@ -477,6 +477,14 @@ const serverConfigSchema = defineConfig({
477
477
  max: field(z.number().default(100), { env: "FIRST_TREE_HUB_RATE_LIMIT_MAX" }),
478
478
  loginMax: field(z.number().default(5), { env: "FIRST_TREE_HUB_RATE_LIMIT_LOGIN_MAX" }),
479
479
  webhookMax: field(z.number().default(60), { env: "FIRST_TREE_HUB_RATE_LIMIT_WEBHOOK_MAX" })
480
+ }),
481
+ kael: optional({
482
+ endpoint: field(z.string(), { env: "KAEL_ENDPOINT" }),
483
+ apiKey: field(z.string(), {
484
+ env: "KAEL_API_KEY",
485
+ secret: true
486
+ }),
487
+ hubPublicUrl: field(z.string(), { env: "FIRST_TREE_HUB_PUBLIC_URL" })
480
488
  })
481
489
  });
482
490
  //#endregion
@@ -580,4 +588,4 @@ async function checkBootstrapStatus(serverUrl, agentId) {
580
588
  return await res.json();
581
589
  }
582
590
  //#endregion
583
- export { resetConfig as _, getGitHubUsername as a, serverConfigSchema as b, DEFAULT_CONFIG_DIR as c, clientConfigSchema as d, collectMissingPrompts as f, readConfigFile as g, loadAgents as h, getGitHubToken as i, DEFAULT_DATA_DIR as l, initConfig as m, bootstrap_exports as n, resolveAgentToken as o, getConfigValue as p, checkBootstrapStatus as r, resolveServerUrl as s, bootstrapToken as t, agentConfigSchema as u, resetConfigMeta as v, setConfigValue as x, resolveConfigReadonly as y };
591
+ export { setConfigValue as S, readConfigFile as _, getGitHubUsername as a, resolveConfigReadonly as b, DEFAULT_CONFIG_DIR as c, agentConfigSchema as d, clientConfigSchema as f, loadAgents as g, initConfig as h, getGitHubToken as i, DEFAULT_DATA_DIR as l, getConfigValue as m, bootstrap_exports as n, resolveAgentToken as o, collectMissingPrompts as p, checkBootstrapStatus as r, resolveServerUrl as s, bootstrapToken as t, DEFAULT_HOME_DIR as u, resetConfig as v, serverConfigSchema as x, resetConfigMeta as y };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { A as ClientRuntime, C as checkWebSocket, F as SessionRegistry, I as cleanWorkspaces, N as FirstTreeHubSDK, P as SdkError, S as checkServerReachable, _ as checkDocker, a as formatCheckReport, b as checkServerConfig, c as onboardContinue, d as runMigrations, f as checkAgentConfigs, g as checkDatabase, h as checkContextTreeRepo, i as promptMissingFields, j as createAdminUser, k as stopPostgres, l as onboardCreate, m as checkClientConfig, n as isInteractive, o as loadOnboardState, p as checkAgentTokens, r as promptAddAgent, s as onboardCheck, t as startServer, u as saveOnboardState, v as checkGitHubToken, w as printResults, x as checkServerHealth, y as checkNodeVersion } from "../core-CZjUVAU-.mjs";
3
- import { _ as resetConfig, b as serverConfigSchema, c as DEFAULT_CONFIG_DIR, d as clientConfigSchema, g as readConfigFile, h as loadAgents, l as DEFAULT_DATA_DIR, m as initConfig, o as resolveAgentToken, p as getConfigValue, s as resolveServerUrl, t as bootstrapToken, u as agentConfigSchema, v as resetConfigMeta, x as setConfigValue } from "../bootstrap-CPdLNPme.mjs";
2
+ import { A as ClientRuntime, C as checkWebSocket, F as SessionRegistry, I as cleanWorkspaces, N as FirstTreeHubSDK, P as SdkError, S as checkServerReachable, _ as checkDocker, a as formatCheckReport, b as checkServerConfig, c as onboardContinue, d as runMigrations, f as checkAgentConfigs, g as checkDatabase, h as checkContextTreeRepo, i as promptMissingFields, j as createAdminUser, k as stopPostgres, l as onboardCreate, m as checkClientConfig, n as isInteractive, o as loadOnboardState, p as checkAgentTokens, r as promptAddAgent, s as onboardCheck, t as startServer, u as saveOnboardState, v as checkGitHubToken, w as printResults, x as checkServerHealth, y as checkNodeVersion } from "../core-CHL_dgzu.mjs";
3
+ import { S as setConfigValue, _ as readConfigFile, c as DEFAULT_CONFIG_DIR, d as agentConfigSchema, f as clientConfigSchema, g as loadAgents, h as initConfig, l as DEFAULT_DATA_DIR, m as getConfigValue, o as resolveAgentToken, s as resolveServerUrl, t as bootstrapToken, u as DEFAULT_HOME_DIR, v as resetConfig, x as serverConfigSchema, y as resetConfigMeta } from "../bootstrap-mhkpeOEc.mjs";
4
4
  import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-Y4m2zFc3.mjs";
5
5
  import { createRequire } from "node:module";
6
6
  import { Command } from "commander";
@@ -168,7 +168,7 @@ function registerAgentCommands(program) {
168
168
  agent.command("token").description("Agent token management").command("bootstrap <agentId>").description("Bootstrap a token using GitHub identity (requires gh CLI)").option("--save-to <target>", "Save token to: \"agent\" (default) or a file path", "agent").option("--server <url>", "Hub server URL").action(async (agentId, options) => {
169
169
  try {
170
170
  const result = await bootstrapToken(resolveServerUrl(options.server), agentId, { saveTo: options.saveTo });
171
- if (options.saveTo === "agent") process.stderr.write(`Token saved to ~/.first-tree-hub/config/agents/${agentId}/agent.yaml\n`);
171
+ if (options.saveTo === "agent") process.stderr.write(`Token saved to ${DEFAULT_HOME_DIR}/config/agents/${agentId}/agent.yaml\n`);
172
172
  else process.stderr.write(`Token saved to ${options.saveTo}\n`);
173
173
  success({
174
174
  agentId: result.agentId,
@@ -457,7 +457,7 @@ function isSecretField(schema, dotPath) {
457
457
  async function promptMissing(args) {
458
458
  let ghUsername = null;
459
459
  try {
460
- const { getGitHubUsername } = await import("../bootstrap-CPdLNPme.mjs").then((n) => n.n);
460
+ const { getGitHubUsername } = await import("../bootstrap-mhkpeOEc.mjs").then((n) => n.n);
461
461
  ghUsername = getGitHubUsername();
462
462
  } catch {}
463
463
  if (!args.id) {
@@ -502,7 +502,7 @@ async function promptMissing(args) {
502
502
  saveOnboardState(args);
503
503
  }
504
504
  }
505
- if (!args.assistant) {
505
+ if (!args.assistant && args.type === "human") {
506
506
  if (await confirm({
507
507
  message: "Create a personal assistant?",
508
508
  default: false
@@ -515,13 +515,13 @@ async function promptMissing(args) {
515
515
  }
516
516
  }
517
517
  if (!args.server) try {
518
- const { resolveServerUrl } = await import("../bootstrap-CPdLNPme.mjs").then((n) => n.n);
518
+ const { resolveServerUrl } = await import("../bootstrap-mhkpeOEc.mjs").then((n) => n.n);
519
519
  resolveServerUrl();
520
520
  } catch {
521
521
  args.server = await input({ message: "Hub server URL:" });
522
522
  saveOnboardState(args);
523
523
  }
524
- if (!args.feishuBotAppId) {
524
+ if (!args.feishuBotAppId && (args.type !== "human" || args.assistant)) {
525
525
  if (await confirm({
526
526
  message: "Bind Feishu bot?",
527
527
  default: false
@@ -1,4 +1,4 @@
1
- import { a as getGitHubUsername, b as serverConfigSchema, c as DEFAULT_CONFIG_DIR, d as clientConfigSchema, f as collectMissingPrompts, h as loadAgents, m as initConfig, r as checkBootstrapStatus, s as resolveServerUrl, t as bootstrapToken$1, u as agentConfigSchema, x as setConfigValue, y as resolveConfigReadonly } from "./bootstrap-CPdLNPme.mjs";
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";
2
2
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { EventEmitter } from "node:events";
@@ -395,7 +395,7 @@ defineConfig({
395
395
  "error"
396
396
  ]).default("info"), { env: "FIRST_TREE_HUB_LOG_LEVEL" })
397
397
  });
398
- 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");
399
399
  join(DEFAULT_HOME_DIR, "config");
400
400
  const DEFAULT_DATA_DIR = join(DEFAULT_HOME_DIR, "data");
401
401
  defineConfig({
@@ -460,6 +460,14 @@ defineConfig({
460
460
  max: field(z.number().default(100), { env: "FIRST_TREE_HUB_RATE_LIMIT_MAX" }),
461
461
  loginMax: field(z.number().default(5), { env: "FIRST_TREE_HUB_RATE_LIMIT_LOGIN_MAX" }),
462
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" })
463
471
  })
464
472
  });
465
473
  join(DEFAULT_DATA_DIR, "context-tree");
@@ -2003,10 +2011,10 @@ async function runMigrations(databaseUrl) {
2003
2011
  }
2004
2012
  //#endregion
2005
2013
  //#region src/core/onboard.ts
2006
- const STATE_FILE = join(homedir(), ".first-tree-hub", ".onboard-state.json");
2014
+ const STATE_FILE = join(DEFAULT_HOME_DIR$1, ".onboard-state.json");
2007
2015
  /** Save current onboard args to state file for resume. */
2008
2016
  function saveOnboardState(args) {
2009
- mkdirSync(join(homedir(), ".first-tree-hub"), { recursive: true });
2017
+ mkdirSync(DEFAULT_HOME_DIR$1, { recursive: true });
2010
2018
  writeFileSync(STATE_FILE, JSON.stringify({ args }, null, 2));
2011
2019
  }
2012
2020
  /** Load saved onboard args from state file. */
@@ -2139,7 +2147,7 @@ async function onboardCheck(args) {
2139
2147
  status: "missing_optional",
2140
2148
  hint: `defaults to "${args.id ?? ""}"`
2141
2149
  });
2142
- items.push(args.assistant ? {
2150
+ if (args.type === "human") items.push(args.assistant ? {
2143
2151
  key: "assistant",
2144
2152
  label: "assistant",
2145
2153
  status: "ok",
@@ -2150,7 +2158,7 @@ async function onboardCheck(args) {
2150
2158
  status: "missing_optional",
2151
2159
  hint: "Also create a personal_assistant"
2152
2160
  });
2153
- items.push(args.feishuBotAppId ? {
2161
+ if (args.type !== "human" || args.assistant) items.push(args.feishuBotAppId ? {
2154
2162
  key: "feishu_bot",
2155
2163
  label: "feishu-bot-app-id",
2156
2164
  status: "ok",
@@ -2159,7 +2167,7 @@ async function onboardCheck(args) {
2159
2167
  key: "feishu_bot",
2160
2168
  label: "feishu-bot-app-id",
2161
2169
  status: "missing_optional",
2162
- hint: "Feishu bot App ID for assistant"
2170
+ hint: "Feishu bot App ID"
2163
2171
  });
2164
2172
  if (args.id && repoPath) if (existsSync(join(repoPath, "members", args.id))) try {
2165
2173
  execSync(`git ls-files --error-unmatch members/${args.id}/NODE.md`, {
@@ -2201,6 +2209,7 @@ function formatCheckReport(items) {
2201
2209
  async function onboardCreate(args) {
2202
2210
  const repoPath = await resolveContextTreeRepo(args.server);
2203
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}`);
2204
2213
  const ghUsername = getGitHubUsername();
2205
2214
  const githubField = args.type === "human" ? ghUsername : null;
2206
2215
  const humanNodePath = join(repoPath, "members", args.id, "NODE.md");
@@ -2323,7 +2332,7 @@ async function onboardCreate(args) {
2323
2332
  branch,
2324
2333
  prUrl: prOutput
2325
2334
  };
2326
- mkdirSync(join(homedir(), ".first-tree-hub"), { recursive: true });
2335
+ mkdirSync(DEFAULT_HOME_DIR$1, { recursive: true });
2327
2336
  writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
2328
2337
  return { prUrl: prOutput };
2329
2338
  }
@@ -2366,7 +2375,7 @@ async function onboardContinue(args) {
2366
2375
  first-tree-hub onboard --continue`);
2367
2376
  throw err;
2368
2377
  }
2369
- 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`);
2370
2379
  if (mergedArgs.feishuBotAppId && mergedArgs.feishuBotAppSecret) {
2371
2380
  const { bindFeishuBot } = await import("./feishu-Y4m2zFc3.mjs").then((n) => n.r);
2372
2381
  process.stderr.write("Binding Feishu bot...\n");
@@ -2377,10 +2386,11 @@ async function onboardContinue(args) {
2377
2386
  const { unlinkSync } = await import("node:fs");
2378
2387
  unlinkSync(STATE_FILE);
2379
2388
  } catch {}
2389
+ const typeLabel = mergedArgs.type === "human" ? "Human" : mergedArgs.type === "autonomous_agent" ? "Agent" : "Assistant";
2380
2390
  process.stderr.write("\n✅ Onboard complete!\n\n");
2381
- process.stderr.write(` Human: ${mergedArgs.id}\n`);
2391
+ process.stderr.write(` ${typeLabel}:${" ".repeat(Math.max(1, 10 - typeLabel.length))}${mergedArgs.id}\n`);
2382
2392
  if (mergedArgs.assistant) process.stderr.write(` Assistant: ${mergedArgs.assistant}\n`);
2383
- process.stderr.write(` Token: ~/.first-tree-hub/config/agents/${agentToBootstrap}/agent.yaml\n`);
2393
+ process.stderr.write(` Token: ${DEFAULT_HOME_DIR$1}/config/agents/${agentToBootstrap}/agent.yaml\n`);
2384
2394
  if (mergedArgs.feishuBotAppId) process.stderr.write(` Feishu: bot bound (${mergedArgs.feishuBotAppId})\n`);
2385
2395
  setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", serverUrl);
2386
2396
  if (mergedArgs.type === "human") {
@@ -2397,7 +2407,16 @@ function createMemberNodeMd(repoPath, data) {
2397
2407
  mkdirSync(memberDir, { recursive: true });
2398
2408
  const domainsList = data.domains.map((d) => ` - "${d}"`).join("\n");
2399
2409
  const githubLine = data.github ? `\ngithub: ${data.github}` : "";
2400
- 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
+ `;
2401
2420
  const content = `---
2402
2421
  title: "${data.displayName}"
2403
2422
  owners: [${data.owner}]
@@ -2409,10 +2428,7 @@ ${domainsList}${githubLine}${delegateLine}
2409
2428
 
2410
2429
  # ${data.displayName}
2411
2430
 
2412
- ## About
2413
-
2414
- ## Current Focus
2415
- `;
2431
+ ${bodySections}`;
2416
2432
  writeFileSync(join(memberDir, "NODE.md"), content);
2417
2433
  }
2418
2434
  function isTrackedByGit(repoPath, filePath) {
@@ -2426,9 +2442,9 @@ function isTrackedByGit(repoPath, filePath) {
2426
2442
  return false;
2427
2443
  }
2428
2444
  }
2429
- const CONTEXT_TREE_DIR = join(homedir(), ".first-tree-hub", "context-tree");
2445
+ const CONTEXT_TREE_DIR = join(DEFAULT_HOME_DIR$1, "context-tree");
2430
2446
  /**
2431
- * 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/.
2432
2448
  *
2433
2449
  * Repo URL is obtained from the Hub server. The local clone is always
2434
2450
  * managed in the standard location — no custom paths allowed.
@@ -2475,13 +2491,13 @@ async function resolveContextTreeRepo(serverUrl) {
2475
2491
  return CONTEXT_TREE_DIR;
2476
2492
  }
2477
2493
  } catch {}
2478
- const safePrefix = join(homedir(), ".first-tree-hub");
2494
+ const safePrefix = DEFAULT_HOME_DIR$1;
2479
2495
  if (!CONTEXT_TREE_DIR.startsWith(safePrefix) || CONTEXT_TREE_DIR === safePrefix) throw new Error(`Refusing to delete unsafe path: ${CONTEXT_TREE_DIR}`);
2480
2496
  execSync(`rm -rf ${CONTEXT_TREE_DIR}`);
2481
2497
  }
2482
2498
  try {
2483
2499
  process.stderr.write(`Cloning Context Tree to ${CONTEXT_TREE_DIR}...\n`);
2484
- mkdirSync(join(homedir(), ".first-tree-hub"), { recursive: true });
2500
+ mkdirSync(DEFAULT_HOME_DIR$1, { recursive: true });
2485
2501
  execSync(`git ${gitConfigArgs} clone ${repoUrl} ${CONTEXT_TREE_DIR}`, {
2486
2502
  stdio: "pipe",
2487
2503
  env: gitEnv
@@ -2546,8 +2562,7 @@ async function promptMissingFields(options) {
2546
2562
  const envStr = envHint ? ` (env: ${envHint})` : "";
2547
2563
  return ` ${m.dotPath}${envStr}`;
2548
2564
  });
2549
- throw new Error(`Missing required configuration:\n${lines.join("\n")}\n\nProvide values via environment variables, config file (~/.first-tree-hub/server.yaml),
2550
- 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.`);
2551
2566
  }
2552
2567
  const configPath = join(options.configDir ?? DEFAULT_CONFIG_DIR, `${options.role}.yaml`);
2553
2568
  const results = {};
@@ -2627,7 +2642,11 @@ function setNestedByDot(obj, dotPath, value) {
2627
2642
  }
2628
2643
  //#endregion
2629
2644
  //#region ../shared/dist/index.mjs
2630
- const adapterPlatformSchema = z.enum(["feishu", "slack"]);
2645
+ const adapterPlatformSchema = z.enum([
2646
+ "feishu",
2647
+ "slack",
2648
+ "kael"
2649
+ ]);
2631
2650
  const adapterStatusSchema = z.enum(["active", "inactive"]);
2632
2651
  const createAdapterConfigSchema = z.object({
2633
2652
  platform: adapterPlatformSchema,
@@ -2900,7 +2919,7 @@ const SYSTEM_CONFIG_DEFAULTS = {
2900
2919
  [SYSTEM_CONFIG_KEYS.PRESENCE_CLEANUP_SECONDS]: 60
2901
2920
  };
2902
2921
  //#endregion
2903
- //#region ../server/dist/app-BVTDWxJE.mjs
2922
+ //#region ../server/dist/app-CurdzcN2.mjs
2904
2923
  var __defProp = Object.defineProperty;
2905
2924
  var __exportAll = (all, no_symbols) => {
2906
2925
  let target = {};
@@ -4832,7 +4851,7 @@ async function pollInbox(db, inboxId, limit) {
4832
4851
  });
4833
4852
  });
4834
4853
  }
4835
- async function ackEntry$1(db, entryId, inboxId) {
4854
+ async function ackEntry$2(db, entryId, inboxId) {
4836
4855
  const [entry] = await db.update(inboxEntries).set({
4837
4856
  status: "acked",
4838
4857
  ackedAt: /* @__PURE__ */ new Date()
@@ -4874,7 +4893,7 @@ async function agentInboxRoutes(app) {
4874
4893
  app.post("/:entryId/ack", async (request, reply) => {
4875
4894
  const identity = requireAgent(request);
4876
4895
  const entryId = Number(request.params.entryId);
4877
- await ackEntry$1(app.db, entryId, identity.inboxId);
4896
+ await ackEntry$2(app.db, entryId, identity.inboxId);
4878
4897
  return reply.status(204).send();
4879
4898
  });
4880
4899
  app.post("/:entryId/renew", async (request, reply) => {
@@ -5647,7 +5666,7 @@ async function withoutProxy(fn) {
5647
5666
  for (const [key, val] of Object.entries(saved)) process.env[key] = val;
5648
5667
  }
5649
5668
  }
5650
- const OUTBOUND_BATCH_SIZE = 10;
5669
+ const OUTBOUND_BATCH_SIZE$1 = 10;
5651
5670
  /** Wrap an SDK API call with proxy bypass if needed. */
5652
5671
  function botApiCall(bot, fn) {
5653
5672
  return bot.bypassProxy ? withoutProxy(fn) : fn();
@@ -5822,6 +5841,8 @@ function parseEventData(appId, data) {
5822
5841
  const sender = data.sender;
5823
5842
  const message = data.message;
5824
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;
5825
5846
  const eventId = data.event_id ?? `ws_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
5826
5847
  let parsedContent;
5827
5848
  try {
@@ -5838,7 +5859,8 @@ function parseEventData(appId, data) {
5838
5859
  eventId,
5839
5860
  platform: "feishu",
5840
5861
  appId,
5841
- senderId: sender.sender_id.open_id,
5862
+ senderId: resolvedSenderId,
5863
+ senderOpenId: sender.sender_id.open_id,
5842
5864
  senderType: sender.sender_type ?? "user",
5843
5865
  externalChannelId: message.chat_id ?? "",
5844
5866
  chatType: message.chat_type ?? "group",
@@ -6011,7 +6033,7 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
6011
6033
  JOIN adapter_agent_mappings aam ON a.id = aam.agent_id
6012
6034
  WHERE aam.platform = 'feishu' AND ie.status = 'pending'
6013
6035
  ORDER BY ie.created_at
6014
- LIMIT ${OUTBOUND_BATCH_SIZE}
6036
+ LIMIT ${OUTBOUND_BATCH_SIZE$1}
6015
6037
  FOR UPDATE SKIP LOCKED
6016
6038
  )
6017
6039
  RETURNING id, inbox_id, message_id, chat_id
@@ -6020,21 +6042,21 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
6020
6042
  for (const entry of claimed) try {
6021
6043
  const [msg] = await db.select().from(messages).where(eq(messages.id, entry.message_id)).limit(1);
6022
6044
  if (!msg) {
6023
- await ackEntry(db, entry.id);
6045
+ await ackEntry$1(db, entry.id);
6024
6046
  continue;
6025
6047
  }
6026
6048
  if (msg.metadata?.source === "feishu") {
6027
- await ackEntry(db, entry.id);
6049
+ await ackEntry$1(db, entry.id);
6028
6050
  continue;
6029
6051
  }
6030
6052
  const channelMapping = await findExternalChannelByChat(db, "feishu", entry.chat_id ?? msg.chatId);
6031
6053
  if (!channelMapping) {
6032
- await ackEntry(db, entry.id);
6054
+ await ackEntry$1(db, entry.id);
6033
6055
  continue;
6034
6056
  }
6035
6057
  const dedupKey = `${msg.id}:${channelMapping.externalChannelId}`;
6036
6058
  if (sentMessages.has(dedupKey)) {
6037
- await ackEntry(db, entry.id);
6059
+ await ackEntry$1(db, entry.id);
6038
6060
  continue;
6039
6061
  }
6040
6062
  const bot = findBotByAgentId(msg.senderId);
@@ -6043,7 +6065,7 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
6043
6065
  messageId: msg.id,
6044
6066
  senderId: msg.senderId
6045
6067
  }, "Outbound skip: sender has no feishu bot binding");
6046
- await ackEntry(db, entry.id);
6068
+ await ackEntry$1(db, entry.id);
6047
6069
  continue;
6048
6070
  }
6049
6071
  const { msgType, content } = formatForFeishu(msg.format, msg.content);
@@ -6063,7 +6085,7 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
6063
6085
  externalMessageId: externalMsgId,
6064
6086
  externalChannelId: channelMapping.externalChannelId
6065
6087
  });
6066
- await ackEntry(db, entry.id);
6088
+ await ackEntry$1(db, entry.id);
6067
6089
  bot.lastActiveAt = /* @__PURE__ */ new Date();
6068
6090
  sent++;
6069
6091
  } catch (err) {
@@ -6078,7 +6100,7 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
6078
6100
  errors: errorCount
6079
6101
  };
6080
6102
  }
6081
- async function ackEntry(db, entryId) {
6103
+ async function ackEntry$1(db, entryId) {
6082
6104
  await db.update(inboxEntries).set({
6083
6105
  status: "acked",
6084
6106
  ackedAt: /* @__PURE__ */ new Date()
@@ -6116,10 +6138,11 @@ function formatForFeishu(format, content) {
6116
6138
  content: JSON.stringify({ text })
6117
6139
  };
6118
6140
  }
6119
- function createBackgroundTasks(app, instanceId, adapterManager) {
6141
+ function createBackgroundTasks(app, instanceId, adapterManager, kaelRuntime) {
6120
6142
  let inboxTimer = null;
6121
6143
  let heartbeatTimer = null;
6122
6144
  let adapterOutboundTimer = null;
6145
+ let kaelOutboundTimer = null;
6123
6146
  return {
6124
6147
  start() {
6125
6148
  inboxTimer = setInterval(async () => {
@@ -6148,6 +6171,13 @@ function createBackgroundTasks(app, instanceId, adapterManager) {
6148
6171
  app.log.error(err, "Adapter outbound processing failed");
6149
6172
  }
6150
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);
6151
6181
  heartbeatInstance(app.db, instanceId).catch((err) => {
6152
6182
  app.log.error(err, "Failed initial heartbeat");
6153
6183
  });
@@ -6165,9 +6195,195 @@ function createBackgroundTasks(app, instanceId, adapterManager) {
6165
6195
  clearInterval(adapterOutboundTimer);
6166
6196
  adapterOutboundTimer = null;
6167
6197
  }
6198
+ if (kaelOutboundTimer) {
6199
+ clearInterval(kaelOutboundTimer);
6200
+ kaelOutboundTimer = null;
6201
+ }
6168
6202
  }
6169
6203
  };
6170
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();
6368
+ }
6369
+ };
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
+ }
6171
6387
  async function buildApp(config) {
6172
6388
  const app = Fastify({ logger: config.logger ?? true });
6173
6389
  const db = connectDatabase(config.database.url);
@@ -6274,14 +6490,19 @@ async function buildApp(config) {
6274
6490
  app.decorate("notifier", notifier);
6275
6491
  const adapterManager = createAdapterManager(db, config.secrets.encryptionKey, app.log, notifier);
6276
6492
  app.decorate("adapterManager", adapterManager);
6277
- 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);
6278
6495
  notifier.onConfigChange((configType) => {
6279
- 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
+ }
6280
6500
  });
6281
6501
  app.addHook("onReady", async () => {
6282
6502
  await notifier.start();
6283
6503
  backgroundTasks.start();
6284
6504
  await adapterManager.reload();
6505
+ await kaelRuntime?.reload();
6285
6506
  const { repo: ctRepo, branch: ctBranch, syncInterval } = config.contextTree;
6286
6507
  const { token: ghToken } = config.github;
6287
6508
  try {
@@ -6299,6 +6520,7 @@ async function buildApp(config) {
6299
6520
  app.addHook("onClose", async () => {
6300
6521
  backgroundTasks.stop();
6301
6522
  adapterManager.shutdown();
6523
+ kaelRuntime?.shutdown();
6302
6524
  await notifier.stop();
6303
6525
  await listenClient.end();
6304
6526
  });
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { A as ClientRuntime, C as checkWebSocket, D as ensurePostgres, E as status, M as hasAdminUser, N as FirstTreeHubSDK, O as isDockerAvailable, P as SdkError, S as checkServerReachable, T as blank, _ as checkDocker, a as formatCheckReport, b as checkServerConfig, c as onboardContinue, d as runMigrations, f as checkAgentConfigs, g as checkDatabase, h as checkContextTreeRepo, i as promptMissingFields, j as createAdminUser, k as stopPostgres, l as onboardCreate, m as checkClientConfig, n as isInteractive, p as checkAgentTokens, r as promptAddAgent, s as onboardCheck, t as startServer, v as checkGitHubToken, w as printResults, x as checkServerHealth, y as checkNodeVersion } from "./core-CZjUVAU-.mjs";
2
- import { a as getGitHubUsername, i as getGitHubToken, o as resolveAgentToken, r as checkBootstrapStatus, s as resolveServerUrl, t as bootstrapToken } from "./bootstrap-CPdLNPme.mjs";
1
+ import { A as ClientRuntime, C as checkWebSocket, D as ensurePostgres, E as status, M as hasAdminUser, N as FirstTreeHubSDK, O as isDockerAvailable, P as SdkError, S as checkServerReachable, T as blank, _ as checkDocker, a as formatCheckReport, b as checkServerConfig, c as onboardContinue, d as runMigrations, f as checkAgentConfigs, g as checkDatabase, h as checkContextTreeRepo, i as promptMissingFields, j as createAdminUser, k as stopPostgres, l as onboardCreate, m as checkClientConfig, n as isInteractive, p as checkAgentTokens, r as promptAddAgent, s as onboardCheck, t as startServer, v as checkGitHubToken, w as printResults, x as checkServerHealth, y as checkNodeVersion } from "./core-CHL_dgzu.mjs";
2
+ import { a as getGitHubUsername, i as getGitHubToken, o as resolveAgentToken, r as checkBootstrapStatus, s as resolveServerUrl, t as bootstrapToken } from "./bootstrap-mhkpeOEc.mjs";
3
3
  import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-Y4m2zFc3.mjs";
4
4
  export { ClientRuntime, FirstTreeHubSDK, SdkError, bindFeishuBot, bindFeishuUser, blank, bootstrapToken, checkAgentConfigs, checkAgentTokens, checkBootstrapStatus, checkClientConfig, checkContextTreeRepo, checkDatabase, checkDocker, checkGitHubToken, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createAdminUser, ensurePostgres, formatCheckReport, getGitHubToken, getGitHubUsername, hasAdminUser, isDockerAvailable, isInteractive, onboardCheck, onboardContinue, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAgentToken, resolveServerUrl, runMigrations, startServer, status, stopPostgres };