@agent-team-foundation/first-tree-hub 0.3.4 → 0.4.0

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,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, s as resolveServerUrl, t as bootstrapToken$1, u as DEFAULT_HOME_DIR$1, x as serverConfigSchema } from "./bootstrap-uyPaaI05.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";
@@ -51,12 +51,13 @@ var FirstTreeHubSDK = class {
51
51
  displayName: agent.displayName,
52
52
  type: agent.type,
53
53
  delegateMention: agent.delegateMention ?? null,
54
+ profile: agent.profile ?? null,
54
55
  metadata: agent.metadata ?? {}
55
56
  };
56
57
  }
57
- /** Fetch Context Tree configuration from the server. */
58
+ /** Fetch Context Tree configuration from the server (public endpoint). */
58
59
  async getContextTreeConfig() {
59
- return this.requestJson("/api/v1/agent/context-tree");
60
+ return this.requestJson("/api/v1/context-tree/info");
60
61
  }
61
62
  /** Fetch pending inbox entries. */
62
63
  async pull(limit = 10) {
@@ -395,7 +396,7 @@ defineConfig({
395
396
  "error"
396
397
  ]).default("info"), { env: "FIRST_TREE_HUB_LOG_LEVEL" })
397
398
  });
398
- const DEFAULT_HOME_DIR = join(homedir(), ".first-tree-hub");
399
+ const DEFAULT_HOME_DIR = process.env.FIRST_TREE_HUB_HOME ?? join(homedir(), ".first-tree-hub");
399
400
  join(DEFAULT_HOME_DIR, "config");
400
401
  const DEFAULT_DATA_DIR = join(DEFAULT_HOME_DIR, "data");
401
402
  defineConfig({
@@ -433,33 +434,37 @@ defineConfig({
433
434
  secret: true
434
435
  })
435
436
  },
436
- contextTree: {
437
+ contextTree: optional({
437
438
  repo: field(z.string(), {
438
439
  env: "FIRST_TREE_HUB_CONTEXT_TREE_REPO",
439
440
  prompt: { message: "Context Tree repo URL (e.g. https://github.com/org/first-tree):" }
440
441
  }),
441
- branch: field(z.string().default("main")),
442
- syncInterval: field(z.number().default(60))
443
- },
442
+ branch: field(z.string().default("main"))
443
+ }),
444
444
  github: {
445
- token: field(z.string(), {
445
+ token: field(z.string().optional(), {
446
446
  env: "FIRST_TREE_HUB_GITHUB_TOKEN",
447
- secret: true,
448
- prompt: {
449
- message: "GitHub token (create at https://github.com/settings/tokens → repo scope):",
450
- type: "password"
451
- }
447
+ secret: true
452
448
  }),
453
449
  webhookSecret: field(z.string().optional(), {
454
450
  env: "FIRST_TREE_HUB_GITHUB_WEBHOOK_SECRET",
455
451
  secret: true
456
- })
452
+ }),
453
+ allowedOrg: field(z.string().optional(), { env: "FIRST_TREE_HUB_GITHUB_ALLOWED_ORG" })
457
454
  },
458
455
  cors: optional({ origin: field(z.string(), { env: "FIRST_TREE_HUB_CORS_ORIGIN" }) }),
459
456
  rateLimit: optional({
460
457
  max: field(z.number().default(100), { env: "FIRST_TREE_HUB_RATE_LIMIT_MAX" }),
461
458
  loginMax: field(z.number().default(5), { env: "FIRST_TREE_HUB_RATE_LIMIT_LOGIN_MAX" }),
462
459
  webhookMax: field(z.number().default(60), { env: "FIRST_TREE_HUB_RATE_LIMIT_WEBHOOK_MAX" })
460
+ }),
461
+ kael: optional({
462
+ endpoint: field(z.string(), { env: "KAEL_ENDPOINT" }),
463
+ apiKey: field(z.string(), {
464
+ env: "KAEL_API_KEY",
465
+ secret: true
466
+ }),
467
+ hubPublicUrl: field(z.string(), { env: "FIRST_TREE_HUB_PUBLIC_URL" })
463
468
  })
464
469
  });
465
470
  join(DEFAULT_DATA_DIR, "context-tree");
@@ -489,14 +494,13 @@ function bootstrapWorkspace(options) {
489
494
  contextTreePath
490
495
  };
491
496
  writeFileSync(join(agentDir, "identity.json"), JSON.stringify(identityData, null, 2), "utf-8");
497
+ if (identity.profile) writeFileSync(join(contextDir, "self.md"), identity.profile, "utf-8");
492
498
  if (contextTreePath) {
493
- const selfNodePath = join(contextTreePath, "members", identity.agentId, "NODE.md");
494
- if (existsSync(selfNodePath)) copyFileSync(selfNodePath, join(contextDir, "self.md"));
495
499
  const agentMdPath = join(contextTreePath, "AGENT.md");
496
500
  if (existsSync(agentMdPath)) copyFileSync(agentMdPath, join(contextDir, "agent-instructions.md"));
497
501
  const rootNodePath = join(contextTreePath, "NODE.md");
498
502
  if (existsSync(rootNodePath)) copyFileSync(rootNodePath, join(contextDir, "domain-map.md"));
499
- } else writeFileSync(join(contextDir, "degraded.md"), "Context Tree is not available for this session.\nOrganizational context, domain structure, and ownership information are not loaded.\n", "utf-8");
503
+ }
500
504
  writeFileSync(join(agentDir, "tools.md"), generateToolsDoc(), "utf-8");
501
505
  }
502
506
  function generateToolsDoc() {
@@ -834,10 +838,9 @@ const createClaudeCodeHandler = (config) => {
834
838
  /**
835
839
  * Generate a CLAUDE.md file from .agent/ bootstrap data.
836
840
  *
837
- * Layered Bootstrap:
838
- * Layer 1 (always): Agent identity + member profile + AGENT.md operating instructions
839
- * Layer 2 (if available): Organization domain map from root NODE.md
840
- * Layer 3 (on-demand): Agent reads specific domain nodes via contextTreePath
841
+ * Layer 1 (always): Agent identity + profile (from Hub)
842
+ * Layer 2 (if Context Tree configured): Operating instructions + domain map
843
+ * Layer 3 (if Context Tree configured): Context Tree location for on-demand reading
841
844
  */
842
845
  function generateClaudeMd(workspacePath, identity, contextTreePath) {
843
846
  const sections = [];
@@ -849,25 +852,18 @@ function generateClaudeMd(workspacePath, identity, contextTreePath) {
849
852
  if (existsSync(selfMdPath)) {
850
853
  const selfContent = readFileSync(selfMdPath, "utf-8");
851
854
  sections.push(`## Your Profile\n\n${selfContent}\n`);
852
- } else sections.push("## Your Profile\n\nNo member profile available. Your responsibilities are not loaded from the Context Tree.\n");
855
+ }
853
856
  const agentInstructionsPath = join(contextDir, "agent-instructions.md");
854
857
  if (existsSync(agentInstructionsPath)) {
855
858
  const instructions = readFileSync(agentInstructionsPath, "utf-8");
856
- sections.push(`## Context Tree Operating Instructions\n\n${instructions}\n`);
857
- } else sections.push("## Context Tree Operating Instructions\n\nContext Tree instructions unavailable. Organizational context is not loaded for this session.\n");
859
+ sections.push(`## Operating Instructions\n\n${instructions}\n`);
860
+ }
858
861
  const domainMapPath = join(contextDir, "domain-map.md");
859
862
  if (existsSync(domainMapPath)) {
860
863
  const domainMap = readFileSync(domainMapPath, "utf-8");
861
864
  sections.push(`## Organization Domain Map\n\n${domainMap}\n`);
862
865
  }
863
866
  if (contextTreePath) sections.push(`## Context Tree Location\n\nThe full Context Tree is available at: \`${contextTreePath}\`\n\nRead specific domain nodes as needed following the operating instructions above.\n`);
864
- else {
865
- const degradedPath = join(contextDir, "degraded.md");
866
- if (existsSync(degradedPath)) {
867
- const degradedMsg = readFileSync(degradedPath, "utf-8");
868
- sections.push(`## Context Tree Location\n\nWARNING: ${degradedMsg}\nYou can still use the SDK tools below, but you lack organizational context for decisions.\n`);
869
- }
870
- }
871
867
  const toolsPath = join(workspacePath, ".agent", "tools.md");
872
868
  if (existsSync(toolsPath)) {
873
869
  const toolsContent = readFileSync(toolsPath, "utf-8");
@@ -1291,6 +1287,7 @@ var AgentSlot = class {
1291
1287
  displayName: agent.displayName,
1292
1288
  type: agent.type,
1293
1289
  delegateMention: agent.delegateMention,
1290
+ profile: agent.profile,
1294
1291
  metadata: agent.metadata
1295
1292
  },
1296
1293
  sdk: this.connection.sdk,
@@ -2003,10 +2000,10 @@ async function runMigrations(databaseUrl) {
2003
2000
  }
2004
2001
  //#endregion
2005
2002
  //#region src/core/onboard.ts
2006
- const STATE_FILE = join(homedir(), ".first-tree-hub", ".onboard-state.json");
2003
+ const STATE_FILE = join(DEFAULT_HOME_DIR$1, ".onboard-state.json");
2007
2004
  /** Save current onboard args to state file for resume. */
2008
2005
  function saveOnboardState(args) {
2009
- mkdirSync(join(homedir(), ".first-tree-hub"), { recursive: true });
2006
+ mkdirSync(DEFAULT_HOME_DIR$1, { recursive: true });
2010
2007
  writeFileSync(STATE_FILE, JSON.stringify({ args }, null, 2));
2011
2008
  }
2012
2009
  /** Load saved onboard args from state file. */
@@ -2050,14 +2047,32 @@ async function onboardCheck(args) {
2050
2047
  key: "server_reachable",
2051
2048
  label: "Server reachable",
2052
2049
  status: res.ok ? "ok" : "error",
2053
- value: res.ok ? "yes" : `HTTP ${res.status}`
2050
+ value: res.ok ? "healthy" : `HTTP ${res.status}`
2054
2051
  });
2052
+ if (res.ok) try {
2053
+ const configRes = await fetch(`${serverUrl}/api/v1/bootstrap/config`);
2054
+ if (configRes.ok) {
2055
+ const config = await configRes.json();
2056
+ if (config.allowedOrg) items.push({
2057
+ key: "allowed_org",
2058
+ label: "GitHub org",
2059
+ status: "ok",
2060
+ value: config.allowedOrg
2061
+ });
2062
+ else items.push({
2063
+ key: "allowed_org",
2064
+ label: "GitHub org",
2065
+ status: "error",
2066
+ hint: "FIRST_TREE_HUB_GITHUB_ALLOWED_ORG not configured on server"
2067
+ });
2068
+ }
2069
+ } catch {}
2055
2070
  } catch {
2056
2071
  items.push({
2057
2072
  key: "server_reachable",
2058
2073
  label: "Server reachable",
2059
2074
  status: "error",
2060
- value: "no"
2075
+ hint: "Cannot connect to server"
2061
2076
  });
2062
2077
  }
2063
2078
  } catch {
@@ -2065,126 +2080,32 @@ async function onboardCheck(args) {
2065
2080
  key: "server",
2066
2081
  label: "Server URL",
2067
2082
  status: "missing_required",
2068
- hint: "--server <url> or FIRST_TREE_HUB_SERVER"
2069
- });
2070
- }
2071
- const repoPath = await resolveContextTreeRepo(args.server);
2072
- if (repoPath) items.push({
2073
- key: "repo",
2074
- label: "Context Tree repo",
2075
- status: "ok",
2076
- value: repoPath
2077
- });
2078
- else {
2079
- const serverAvailable = items.some((i) => i.key === "server" && i.status === "ok");
2080
- items.push({
2081
- key: "repo",
2082
- label: "Context Tree repo",
2083
- status: "missing_required",
2084
- hint: serverAvailable ? "auto-clone failed (check server Context Tree config and gh auth)" : "configure --server first (repo will be auto-cloned from server)"
2083
+ hint: "Provide via --server, FIRST_TREE_HUB_SERVER, or config"
2085
2084
  });
2086
2085
  }
2087
- items.push(args.id ? {
2086
+ if (args.id) items.push({
2088
2087
  key: "id",
2089
- label: "id",
2088
+ label: "Agent ID",
2090
2089
  status: "ok",
2091
2090
  value: args.id
2092
- } : {
2091
+ });
2092
+ else items.push({
2093
2093
  key: "id",
2094
- label: "id",
2094
+ label: "Agent ID",
2095
2095
  status: "missing_required",
2096
- hint: "Member directory name"
2096
+ hint: "Provide via --id"
2097
2097
  });
2098
- items.push(args.type ? {
2098
+ if (args.type) items.push({
2099
2099
  key: "type",
2100
- label: "type",
2100
+ label: "Agent type",
2101
2101
  status: "ok",
2102
2102
  value: args.type
2103
- } : {
2104
- key: "type",
2105
- label: "type",
2106
- status: "missing_required",
2107
- hint: "human | personal_assistant | autonomous_agent"
2108
- });
2109
- items.push(args.role ? {
2110
- key: "role",
2111
- label: "role",
2112
- status: "ok",
2113
- value: args.role
2114
- } : {
2115
- key: "role",
2116
- label: "role",
2117
- status: "missing_required",
2118
- hint: "e.g. \"Engineer\""
2119
- });
2120
- items.push(args.domains ? {
2121
- key: "domains",
2122
- label: "domains",
2123
- status: "ok",
2124
- value: args.domains
2125
- } : {
2126
- key: "domains",
2127
- label: "domains",
2128
- status: "missing_required",
2129
- hint: "Comma-separated, e.g. \"backend,infra\""
2130
2103
  });
2131
- items.push(args.displayName ? {
2132
- key: "display_name",
2133
- label: "display-name",
2134
- status: "ok",
2135
- value: args.displayName
2136
- } : {
2137
- key: "display_name",
2138
- label: "display-name",
2139
- status: "missing_optional",
2140
- hint: `defaults to "${args.id ?? ""}"`
2141
- });
2142
- items.push(args.assistant ? {
2143
- key: "assistant",
2144
- label: "assistant",
2145
- status: "ok",
2146
- value: args.assistant
2147
- } : {
2148
- key: "assistant",
2149
- label: "assistant",
2150
- status: "missing_optional",
2151
- hint: "Also create a personal_assistant"
2152
- });
2153
- items.push(args.feishuBotAppId ? {
2154
- key: "feishu_bot",
2155
- label: "feishu-bot-app-id",
2156
- status: "ok",
2157
- value: args.feishuBotAppId
2158
- } : {
2159
- key: "feishu_bot",
2160
- label: "feishu-bot-app-id",
2161
- status: "missing_optional",
2162
- hint: "Feishu bot App ID for assistant"
2163
- });
2164
- if (args.id && repoPath) if (existsSync(join(repoPath, "members", args.id))) try {
2165
- execSync(`git ls-files --error-unmatch members/${args.id}/NODE.md`, {
2166
- cwd: repoPath,
2167
- stdio: "pipe"
2168
- });
2169
- items.push({
2170
- key: "conflict",
2171
- label: `ID "${args.id}" availability`,
2172
- status: "warning",
2173
- value: "already exists (will overwrite)"
2174
- });
2175
- } catch {
2176
- items.push({
2177
- key: "conflict",
2178
- label: `ID "${args.id}" availability`,
2179
- status: "ok",
2180
- value: "resuming (local files from previous run)"
2181
- });
2182
- }
2183
2104
  else items.push({
2184
- key: "conflict",
2185
- label: `ID "${args.id}" availability`,
2186
- status: "ok",
2187
- value: "available"
2105
+ key: "type",
2106
+ label: "Agent type",
2107
+ status: "missing_required",
2108
+ hint: "Provide via --type"
2188
2109
  });
2189
2110
  return items;
2190
2111
  }
@@ -2199,322 +2120,74 @@ function formatCheckReport(items) {
2199
2120
  return lines.join("\n");
2200
2121
  }
2201
2122
  async function onboardCreate(args) {
2202
- const repoPath = await resolveContextTreeRepo(args.server);
2203
- if (!repoPath) throw new Error("Context Tree repo not available. Ensure --server is configured and the server is running.");
2204
- const ghUsername = getGitHubUsername();
2205
- const githubField = args.type === "human" ? ghUsername : null;
2206
- const humanNodePath = join(repoPath, "members", args.id, "NODE.md");
2207
- if (existsSync(humanNodePath) && isTrackedByGit(repoPath, join("members", args.id, "NODE.md"))) {
2208
- process.stderr.write(`Member "${args.id}" already exists, skipping NODE.md creation.\n`);
2209
- if (args.assistant) {
2210
- const existingContent = readFileSync(humanNodePath, "utf-8");
2211
- if (!existingContent.includes("delegate_mention")) {
2212
- writeFileSync(humanNodePath, existingContent.replace(/^(---\n[\s\S]*?)(---)/m, `$1delegate_mention: ${args.assistant}\n$2`));
2213
- process.stderr.write(`Updated delegate_mention → ${args.assistant}\n`);
2214
- }
2215
- }
2216
- } else createMemberNodeMd(repoPath, {
2217
- id: args.id,
2218
- type: args.type,
2219
- displayName: args.displayName ?? args.id,
2220
- role: args.role,
2221
- domains: args.domains.split(",").map((d) => d.trim()),
2222
- owner: ghUsername,
2223
- github: githubField,
2224
- delegateMention: args.assistant ?? args.delegateMention ?? null
2225
- });
2226
- 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`);
2227
- else createMemberNodeMd(repoPath, {
2228
- parentPath: join("members", args.id),
2229
- id: args.assistant,
2230
- type: "personal_assistant",
2231
- displayName: args.assistant,
2232
- role: `Personal Assistant to ${args.id}`,
2233
- domains: ["message triage", "task coordination"],
2234
- owner: ghUsername,
2235
- github: null,
2236
- delegateMention: null
2237
- });
2123
+ const serverUrl = resolveServerUrl(args.server).replace(/\/+$/, "");
2124
+ getGitHubUsername();
2125
+ process.stderr.write(`Bootstrapping agent "${args.id}"...\n`);
2126
+ const metadata = {};
2127
+ if (args.role) metadata.role = args.role;
2128
+ if (args.domains) metadata.domains = args.domains.split(",").map((d) => d.trim());
2129
+ let token;
2238
2130
  try {
2239
- execSync("npx -y first-tree verify", {
2240
- cwd: repoPath,
2241
- stdio: "pipe"
2242
- });
2131
+ token = (await bootstrapToken$1(serverUrl, args.id, {
2132
+ saveTo: "agent",
2133
+ type: args.type,
2134
+ displayName: args.displayName ?? args.id,
2135
+ profile: args.profile,
2136
+ delegateMention: args.assistant ?? args.delegateMention,
2137
+ metadata: Object.keys(metadata).length > 0 ? metadata : void 0
2138
+ })).token;
2243
2139
  } catch (err) {
2244
- const stderr = err instanceof Error && "stderr" in err ? err.stderr.toString() : "";
2245
- const stdout = err instanceof Error && "stdout" in err ? err.stdout.toString() : "";
2246
- const output = stderr || stdout || String(err);
2247
- 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);
2248
- throw new Error(`Verification failed:\n${output}`);
2249
- }
2250
- const baseBranch = `onboard/${args.id}`;
2251
- let branch = baseBranch;
2252
- const branchExists = (name) => {
2253
- try {
2254
- execSync(`git rev-parse --verify ${name}`, {
2255
- cwd: repoPath,
2256
- stdio: "pipe"
2257
- });
2258
- return true;
2259
- } catch {
2260
- return false;
2261
- }
2262
- };
2263
- if (branchExists(branch)) branch = `${baseBranch}-${Date.now().toString(36)}`;
2264
- try {
2265
- execSync("git checkout main", {
2266
- cwd: repoPath,
2267
- stdio: "pipe"
2268
- });
2269
- } catch {
2270
- try {
2271
- execSync("git checkout master", {
2272
- cwd: repoPath,
2273
- stdio: "pipe"
2274
- });
2275
- } catch {}
2276
- }
2277
- execSync(`git checkout -b ${branch}`, {
2278
- cwd: repoPath,
2279
- stdio: "pipe"
2280
- });
2281
- execSync(`git add members/${args.id}`, {
2282
- cwd: repoPath,
2283
- stdio: "pipe"
2284
- });
2285
- execFileSync("git", [
2286
- "commit",
2287
- "-m",
2288
- args.assistant ? `feat: onboard ${args.id} + ${args.assistant}` : `feat: onboard ${args.id}`
2289
- ], {
2290
- cwd: repoPath,
2291
- stdio: "pipe"
2292
- });
2293
- const pushToken = execSync("gh auth token", {
2294
- encoding: "utf-8",
2295
- stdio: "pipe"
2296
- }).trim();
2297
- const cleanRemote = execSync("git remote get-url origin", {
2298
- cwd: repoPath,
2299
- encoding: "utf-8",
2300
- stdio: "pipe"
2301
- }).trim();
2302
- execSync(`git remote set-url origin "${cleanRemote.replace("https://github.com/", `https://x-access-token:${pushToken}@github.com/`)}"`, {
2303
- cwd: repoPath,
2304
- stdio: "pipe"
2305
- });
2306
- try {
2307
- execSync(`git push -u origin ${branch}`, {
2308
- cwd: repoPath,
2309
- stdio: "pipe"
2310
- });
2311
- } finally {
2312
- execSync(`git remote set-url origin "${cleanRemote}"`, {
2313
- cwd: repoPath,
2314
- stdio: "pipe"
2315
- });
2140
+ const msg = err instanceof Error ? err.message : String(err);
2141
+ if (msg.includes("already has") || msg.includes("409")) throw new Error(`Agent "${args.id}" already has an active token.\nAsk an admin to revoke the existing token in the Web UI, then re-run onboard.`);
2142
+ throw err;
2316
2143
  }
2317
- const prOutput = execSync(`gh pr create --title "${args.assistant ? `Onboard ${args.id} + assistant` : `Onboard ${args.id}`}" --body "Automated onboard via first-tree-hub CLI"`, {
2318
- cwd: repoPath,
2319
- encoding: "utf-8"
2320
- }).trim();
2321
- const state = {
2322
- args,
2323
- branch,
2324
- prUrl: prOutput
2325
- };
2326
- mkdirSync(join(homedir(), ".first-tree-hub"), { recursive: true });
2327
- writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
2328
- return { prUrl: prOutput };
2329
- }
2330
- async function onboardContinue(args) {
2331
- let state = null;
2332
- try {
2333
- state = JSON.parse(readFileSync(STATE_FILE, "utf-8"));
2334
- } catch {}
2335
- if (!state && !args.id) throw new Error("No onboard in progress. Run 'first-tree-hub onboard' first to start a new onboard.");
2336
- const mergedArgs = {
2337
- ...state?.args,
2338
- ...stripUndefined(args)
2339
- };
2340
- const serverUrl = resolveServerUrl(mergedArgs.server).replace(/\/+$/, "");
2341
- const agentToBootstrap = mergedArgs.assistant ?? mergedArgs.id;
2342
- if (!agentToBootstrap) throw new Error("Cannot determine which agent to bootstrap. Provide --id or run onboard first.");
2343
- if (!mergedArgs.id) throw new Error("Cannot determine member ID. Provide --id or run onboard first.");
2344
- process.stderr.write(`Waiting for agent "${agentToBootstrap}" to be synced...\n`);
2345
- let synced = false;
2346
- for (let i = 0; i < 30; i++) {
2144
+ process.stderr.write(`Agent "${args.id}" ready.\n`);
2145
+ if (args.assistant) {
2146
+ process.stderr.write(`Bootstrapping assistant "${args.assistant}"...\n`);
2347
2147
  try {
2348
- const status = await checkBootstrapStatus(serverUrl, agentToBootstrap);
2349
- if (status.exists && status.status === "active") {
2350
- synced = true;
2351
- break;
2352
- }
2148
+ token = (await bootstrapToken$1(serverUrl, args.assistant, {
2149
+ saveTo: "agent",
2150
+ type: "personal_assistant",
2151
+ displayName: args.assistant,
2152
+ metadata: {
2153
+ role: `Personal Assistant to ${args.id}`,
2154
+ domains: ["message triage", "task coordination"]
2155
+ }
2156
+ })).token;
2157
+ process.stderr.write(`Assistant "${args.assistant}" ready.\n`);
2353
2158
  } catch (err) {
2354
- if (i === 0) process.stderr.write(` (check failed: ${err instanceof Error ? err.message : String(err)})\n`);
2159
+ const msg = err instanceof Error ? err.message : String(err);
2160
+ process.stderr.write(`Warning: Failed to bootstrap assistant "${args.assistant}": ${msg}\n`);
2355
2161
  }
2356
- await sleep(2e3);
2357
2162
  }
2358
- if (!synced) throw new Error(`Agent "${agentToBootstrap}" not found after 60s. Trigger sync manually or wait for auto-sync.`);
2359
- process.stderr.write(`Bootstrapping token for "${agentToBootstrap}"...\n`);
2360
- let token;
2361
- try {
2362
- token = (await bootstrapToken$1(serverUrl, agentToBootstrap, { saveTo: "agent" })).token;
2363
- } catch (err) {
2364
- const msg = err instanceof Error ? err.message : String(err);
2365
- 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:
2366
- first-tree-hub onboard --continue`);
2367
- throw err;
2368
- }
2369
- process.stderr.write(`Token saved to ~/.first-tree-hub/agents/${agentToBootstrap}/agent.yaml\n`);
2370
- if (mergedArgs.feishuBotAppId && mergedArgs.feishuBotAppSecret) {
2163
+ const agentToBootstrap = args.assistant ?? args.id;
2164
+ process.stderr.write(`Token saved to ${DEFAULT_HOME_DIR$1}/config/agents/${agentToBootstrap}/agent.yaml\n`);
2165
+ if (args.feishuBotAppId && args.feishuBotAppSecret) {
2371
2166
  const { bindFeishuBot } = await import("./feishu-Y4m2zFc3.mjs").then((n) => n.r);
2372
2167
  process.stderr.write("Binding Feishu bot...\n");
2373
- await bindFeishuBot(serverUrl, token, mergedArgs.feishuBotAppId, mergedArgs.feishuBotAppSecret);
2168
+ await bindFeishuBot(serverUrl, token, args.feishuBotAppId, args.feishuBotAppSecret);
2374
2169
  process.stderr.write("Feishu bot bound.\n");
2375
2170
  }
2171
+ setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", serverUrl);
2376
2172
  try {
2377
2173
  const { unlinkSync } = await import("node:fs");
2378
2174
  unlinkSync(STATE_FILE);
2379
2175
  } catch {}
2176
+ const typeLabel = args.type === "human" ? "Human" : args.type === "autonomous_agent" ? "Agent" : "Assistant";
2380
2177
  process.stderr.write("\n✅ Onboard complete!\n\n");
2381
- process.stderr.write(` Human: ${mergedArgs.id}\n`);
2382
- if (mergedArgs.assistant) process.stderr.write(` Assistant: ${mergedArgs.assistant}\n`);
2383
- process.stderr.write(` Token: ~/.first-tree-hub/config/agents/${agentToBootstrap}/agent.yaml\n`);
2384
- if (mergedArgs.feishuBotAppId) process.stderr.write(` Feishu: bot bound (${mergedArgs.feishuBotAppId})\n`);
2385
- setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", serverUrl);
2386
- if (mergedArgs.type === "human") {
2178
+ process.stderr.write(` ${typeLabel}:${" ".repeat(Math.max(1, 10 - typeLabel.length))}${args.id}\n`);
2179
+ if (args.assistant) process.stderr.write(` Assistant: ${args.assistant}\n`);
2180
+ process.stderr.write(` Token: ${DEFAULT_HOME_DIR$1}/config/agents/${agentToBootstrap}/agent.yaml\n`);
2181
+ if (args.feishuBotAppId) process.stderr.write(` Feishu: bot bound (${args.feishuBotAppId})\n`);
2182
+ if (args.type === "human") {
2387
2183
  process.stderr.write("\n Next step — bind your Feishu account:\n");
2388
- process.stderr.write(` Send this message to the bot in Feishu: /bind ${mergedArgs.id}\n`);
2389
- if (!mergedArgs.feishuBotAppId) process.stderr.write(" (requires a Feishu bot to be configured in the system)\n");
2184
+ process.stderr.write(` Send this message to the bot in Feishu: /bind ${args.id}\n`);
2185
+ if (!args.feishuBotAppId) process.stderr.write(" (requires a Feishu bot to be configured in the system)\n");
2390
2186
  }
2391
2187
  process.stderr.write("\n Start the agent:\n");
2392
2188
  process.stderr.write(" first-tree-hub client start\n");
2393
2189
  process.stderr.write("\n");
2394
2190
  }
2395
- function createMemberNodeMd(repoPath, data) {
2396
- const memberDir = join(repoPath, data.parentPath ?? "members", data.id);
2397
- mkdirSync(memberDir, { recursive: true });
2398
- const domainsList = data.domains.map((d) => ` - "${d}"`).join("\n");
2399
- const githubLine = data.github ? `\ngithub: ${data.github}` : "";
2400
- const delegateLine = data.delegateMention ? `\ndelegate_mention: ${data.delegateMention}` : "";
2401
- const content = `---
2402
- title: "${data.displayName}"
2403
- owners: [${data.owner}]
2404
- type: ${data.type}
2405
- role: "${data.role}"
2406
- domains:
2407
- ${domainsList}${githubLine}${delegateLine}
2408
- ---
2409
-
2410
- # ${data.displayName}
2411
-
2412
- ## About
2413
-
2414
- ## Current Focus
2415
- `;
2416
- writeFileSync(join(memberDir, "NODE.md"), content);
2417
- }
2418
- function isTrackedByGit(repoPath, filePath) {
2419
- try {
2420
- execSync(`git ls-files --error-unmatch ${filePath}`, {
2421
- cwd: repoPath,
2422
- stdio: "pipe"
2423
- });
2424
- return true;
2425
- } catch {
2426
- return false;
2427
- }
2428
- }
2429
- const CONTEXT_TREE_DIR = join(homedir(), ".first-tree-hub", "context-tree");
2430
- /**
2431
- * Resolve Context Tree to a **local path** at ~/.first-tree-hub/context-tree/.
2432
- *
2433
- * Repo URL is obtained from the Hub server. The local clone is always
2434
- * managed in the standard location — no custom paths allowed.
2435
- */
2436
- async function resolveContextTreeRepo(serverUrl) {
2437
- const repoUrl = await fetchRepoUrlFromServer(serverUrl);
2438
- if (!repoUrl) return null;
2439
- let ghToken;
2440
- try {
2441
- ghToken = execSync("gh auth token", {
2442
- encoding: "utf-8",
2443
- stdio: "pipe"
2444
- }).trim();
2445
- } catch {
2446
- return null;
2447
- }
2448
- const gitEnv = {
2449
- ...process.env,
2450
- GIT_ASKPASS: "echo",
2451
- GIT_TERMINAL_PROMPT: "0",
2452
- GH_TOKEN: ghToken,
2453
- GITHUB_TOKEN: ghToken
2454
- };
2455
- const gitConfigArgs = `-c url."https://x-access-token:${ghToken}@github.com/".insteadOf="https://github.com/"`;
2456
- if (existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
2457
- try {
2458
- if (execSync("git remote get-url origin", {
2459
- cwd: CONTEXT_TREE_DIR,
2460
- encoding: "utf-8",
2461
- stdio: "pipe"
2462
- }).trim().includes(repoUrl.replace(/^https?:\/\/github\.com\//, "").replace(/\.git$/, ""))) {
2463
- process.stderr.write("Updating Context Tree...\n");
2464
- execSync("git checkout main 2>/dev/null || git checkout master", {
2465
- cwd: CONTEXT_TREE_DIR,
2466
- stdio: "pipe"
2467
- });
2468
- try {
2469
- execSync(`git ${gitConfigArgs} pull --ff-only`, {
2470
- cwd: CONTEXT_TREE_DIR,
2471
- stdio: "pipe",
2472
- env: gitEnv
2473
- });
2474
- } catch {}
2475
- return CONTEXT_TREE_DIR;
2476
- }
2477
- } catch {}
2478
- const safePrefix = join(homedir(), ".first-tree-hub");
2479
- if (!CONTEXT_TREE_DIR.startsWith(safePrefix) || CONTEXT_TREE_DIR === safePrefix) throw new Error(`Refusing to delete unsafe path: ${CONTEXT_TREE_DIR}`);
2480
- execSync(`rm -rf ${CONTEXT_TREE_DIR}`);
2481
- }
2482
- try {
2483
- process.stderr.write(`Cloning Context Tree to ${CONTEXT_TREE_DIR}...\n`);
2484
- mkdirSync(join(homedir(), ".first-tree-hub"), { recursive: true });
2485
- execSync(`git ${gitConfigArgs} clone ${repoUrl} ${CONTEXT_TREE_DIR}`, {
2486
- stdio: "pipe",
2487
- env: gitEnv
2488
- });
2489
- return CONTEXT_TREE_DIR;
2490
- } catch {
2491
- return null;
2492
- }
2493
- }
2494
- /** Query server for Context Tree repo URL. */
2495
- async function fetchRepoUrlFromServer(serverUrl) {
2496
- if (!serverUrl) try {
2497
- serverUrl = resolveServerUrl();
2498
- } catch {
2499
- return null;
2500
- }
2501
- try {
2502
- const url = `${serverUrl.replace(/\/+$/, "")}/api/v1/context-tree/info`;
2503
- const res = await fetch(url, { signal: AbortSignal.timeout(5e3) });
2504
- if (!res.ok) return null;
2505
- return (await res.json()).repo ?? null;
2506
- } catch {
2507
- return null;
2508
- }
2509
- }
2510
- function sleep(ms) {
2511
- return new Promise((resolve) => setTimeout(resolve, ms));
2512
- }
2513
- function stripUndefined(obj) {
2514
- const result = {};
2515
- for (const [key, value] of Object.entries(obj)) if (value !== void 0) result[key] = value;
2516
- return result;
2517
- }
2518
2191
  //#endregion
2519
2192
  //#region src/core/prompt.ts
2520
2193
  /**
@@ -2546,8 +2219,7 @@ async function promptMissingFields(options) {
2546
2219
  const envStr = envHint ? ` (env: ${envHint})` : "";
2547
2220
  return ` ${m.dotPath}${envStr}`;
2548
2221
  });
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.`);
2222
+ 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
2223
  }
2552
2224
  const configPath = join(options.configDir ?? DEFAULT_CONFIG_DIR, `${options.role}.yaml`);
2553
2225
  const results = {};
@@ -2627,7 +2299,11 @@ function setNestedByDot(obj, dotPath, value) {
2627
2299
  }
2628
2300
  //#endregion
2629
2301
  //#region ../shared/dist/index.mjs
2630
- const adapterPlatformSchema = z.enum(["feishu", "slack"]);
2302
+ const adapterPlatformSchema = z.enum([
2303
+ "feishu",
2304
+ "slack",
2305
+ "kael"
2306
+ ]);
2631
2307
  const adapterStatusSchema = z.enum(["active", "inactive"]);
2632
2308
  const createAdapterConfigSchema = z.object({
2633
2309
  platform: adapterPlatformSchema,
@@ -2731,30 +2407,21 @@ const AGENT_STATUSES = {
2731
2407
  DELETED: "deleted"
2732
2408
  };
2733
2409
  z.enum(["active", "suspended"]);
2734
- z.object({
2410
+ const createAgentSchema = z.object({
2735
2411
  id: z.string().min(1).max(100).regex(/^[a-z0-9_-]+$/, "Only lowercase alphanumeric, hyphens, and underscores").optional(),
2736
2412
  type: agentTypeSchema,
2737
2413
  displayName: z.string().max(200).optional(),
2414
+ delegateMention: z.string().max(100).optional(),
2415
+ profile: z.string().optional(),
2738
2416
  organizationId: z.string().max(100).optional(),
2739
2417
  metadata: z.record(z.string(), z.unknown()).optional()
2740
2418
  });
2741
- z.object({
2742
- syncedAt: z.string(),
2743
- treePath: z.string(),
2744
- summary: z.object({
2745
- created: z.number(),
2746
- updated: z.number(),
2747
- suspended: z.number(),
2748
- unchanged: z.number(),
2749
- errors: z.number()
2750
- }),
2751
- created: z.array(z.string()),
2752
- updated: z.array(z.string()),
2753
- suspended: z.array(z.string()),
2754
- errors: z.array(z.object({
2755
- memberId: z.string(),
2756
- error: z.string()
2757
- }))
2419
+ const updateAgentSchema = z.object({
2420
+ type: agentTypeSchema.optional(),
2421
+ displayName: z.string().max(200).nullable().optional(),
2422
+ delegateMention: z.string().max(100).nullable().optional(),
2423
+ profile: z.string().nullable().optional(),
2424
+ metadata: z.record(z.string(), z.unknown()).optional()
2758
2425
  });
2759
2426
  z.object({
2760
2427
  id: z.string(),
@@ -2762,7 +2429,7 @@ z.object({
2762
2429
  type: agentTypeSchema,
2763
2430
  displayName: z.string().nullable(),
2764
2431
  delegateMention: z.string().nullable(),
2765
- treePath: z.string().nullable(),
2432
+ profile: z.string().nullable(),
2766
2433
  inboxId: z.string(),
2767
2434
  status: z.string(),
2768
2435
  metadata: z.record(z.string(), z.unknown()),
@@ -2770,15 +2437,21 @@ z.object({
2770
2437
  createdAt: z.string(),
2771
2438
  updatedAt: z.string()
2772
2439
  });
2773
- const bootstrapTokenRequestSchema = z.object({ name: z.string().max(100).optional() });
2440
+ const bootstrapTokenRequestSchema = z.object({
2441
+ name: z.string().max(100).optional(),
2442
+ type: agentTypeSchema.optional(),
2443
+ displayName: z.string().max(200).optional(),
2444
+ delegateMention: z.string().max(100).optional(),
2445
+ profile: z.string().optional(),
2446
+ metadata: z.record(z.string(), z.unknown()).optional()
2447
+ });
2774
2448
  z.object({
2775
2449
  exists: z.boolean(),
2776
2450
  status: z.enum(["active", "suspended"]).nullable()
2777
2451
  });
2778
2452
  z.object({
2779
- repo: z.string(),
2780
- branch: z.string(),
2781
- lastSync: z.string().nullable()
2453
+ repo: z.string().nullable(),
2454
+ branch: z.string().nullable()
2782
2455
  });
2783
2456
  const createAgentTokenSchema = z.object({
2784
2457
  name: z.string().max(100).optional(),
@@ -2900,7 +2573,7 @@ const SYSTEM_CONFIG_DEFAULTS = {
2900
2573
  [SYSTEM_CONFIG_KEYS.PRESENCE_CLEANUP_SECONDS]: 60
2901
2574
  };
2902
2575
  //#endregion
2903
- //#region ../server/dist/app-BVTDWxJE.mjs
2576
+ //#region ../server/dist/app-41WnR_ri.mjs
2904
2577
  var __defProp = Object.defineProperty;
2905
2578
  var __exportAll = (all, no_symbols) => {
2906
2579
  let target = {};
@@ -2918,7 +2591,7 @@ const agents = pgTable("agents", {
2918
2591
  type: text("type").notNull(),
2919
2592
  displayName: text("display_name"),
2920
2593
  delegateMention: text("delegate_mention"),
2921
- treePath: text("tree_path"),
2594
+ profile: text("profile"),
2922
2595
  inboxId: text("inbox_id").unique().notNull(),
2923
2596
  status: text("status").notNull().default("active"),
2924
2597
  metadata: jsonb("metadata").$type().notNull().default({}),
@@ -3404,289 +3077,6 @@ async function adminAdapterRoutes(app) {
3404
3077
  return reply.status(204).send();
3405
3078
  });
3406
3079
  }
3407
- const GRAPHQL_URL = "https://api.github.com/graphql";
3408
- const REST_API_URL = "https://api.github.com";
3409
- /** Parse "owner/repo" or "https://github.com/owner/repo" into { owner, name }. */
3410
- function parseRepo(input) {
3411
- const urlMatch = /github\.com\/([^/]+)\/([^/.]+)/.exec(input);
3412
- if (urlMatch) return {
3413
- owner: urlMatch[1] ?? "",
3414
- name: urlMatch[2] ?? ""
3415
- };
3416
- const parts = input.split("/");
3417
- return {
3418
- owner: parts[0] ?? "",
3419
- name: parts[1] ?? ""
3420
- };
3421
- }
3422
- /** Step 1: Get the tree OID of the members/ directory via GraphQL. */
3423
- async function fetchMembersTreeOid(owner, name, branch, token) {
3424
- const query = `
3425
- query($owner: String!, $name: String!, $expr: String!) {
3426
- repository(owner: $owner, name: $name) {
3427
- object(expression: $expr) {
3428
- ... on Tree { oid }
3429
- }
3430
- }
3431
- }
3432
- `;
3433
- const res = await fetch(GRAPHQL_URL, {
3434
- method: "POST",
3435
- headers: {
3436
- Authorization: `Bearer ${token}`,
3437
- "Content-Type": "application/json"
3438
- },
3439
- body: JSON.stringify({
3440
- query,
3441
- variables: {
3442
- owner,
3443
- name,
3444
- expr: `${branch}:members`
3445
- }
3446
- })
3447
- });
3448
- if (!res.ok) throw new Error(`GitHub API returned ${res.status}: ${await res.text()}`);
3449
- const json = await res.json();
3450
- if (json.errors) throw new Error(`GitHub GraphQL errors: ${json.errors.map((e) => e.message).join(", ")}`);
3451
- return json.data?.repository?.object?.oid ?? null;
3452
- }
3453
- /** Step 2: Recursively list all entries under the members/ tree via REST API. */
3454
- async function fetchRecursiveTree(owner, name, treeSha, token) {
3455
- const url = `${REST_API_URL}/repos/${owner}/${name}/git/trees/${treeSha}?recursive=1`;
3456
- const res = await fetch(url, { headers: {
3457
- Authorization: `Bearer ${token}`,
3458
- Accept: "application/vnd.github+json"
3459
- } });
3460
- if (!res.ok) throw new Error(`GitHub REST API returned ${res.status}: ${await res.text()}`);
3461
- const json = await res.json();
3462
- if (json.truncated) throw new Error("[context-tree-sync] GitHub REST tree API returned truncated response — members/ subtree is too large. Sync aborted to prevent incorrect agent suspension from partial data.");
3463
- return json.tree;
3464
- }
3465
- /**
3466
- * Step 3: Batch-fetch NODE.md content for all member directories via GraphQL aliases.
3467
- * Each alias fetches one NODE.md file by expression.
3468
- */
3469
- async function batchFetchNodeMd(owner, name, branch, memberPaths, token) {
3470
- if (memberPaths.length === 0) return /* @__PURE__ */ new Map();
3471
- const query = `
3472
- query($owner: String!, $name: String!) {
3473
- repository(owner: $owner, name: $name) {
3474
- ${memberPaths.map((p, i) => {
3475
- const expr = `${branch}:members/${p}/NODE.md`;
3476
- return `m${i}: object(expression: ${JSON.stringify(expr)}) { ... on Blob { text } }`;
3477
- }).join("\n ")}
3478
- }
3479
- }
3480
- `;
3481
- const res = await fetch(GRAPHQL_URL, {
3482
- method: "POST",
3483
- headers: {
3484
- Authorization: `Bearer ${token}`,
3485
- "Content-Type": "application/json"
3486
- },
3487
- body: JSON.stringify({
3488
- query,
3489
- variables: {
3490
- owner,
3491
- name
3492
- }
3493
- })
3494
- });
3495
- if (!res.ok) throw new Error(`GitHub API returned ${res.status}: ${await res.text()}`);
3496
- const json = await res.json();
3497
- if (json.errors) throw new Error(`GitHub GraphQL errors: ${json.errors.map((e) => e.message).join(", ")}`);
3498
- const repo = json.data?.repository ?? {};
3499
- const result = /* @__PURE__ */ new Map();
3500
- for (let i = 0; i < memberPaths.length; i++) {
3501
- const blob = repo[`m${i}`];
3502
- const path = memberPaths[i];
3503
- if (blob?.text && path) result.set(path, blob.text);
3504
- }
3505
- return result;
3506
- }
3507
- /**
3508
- * Extract member directory paths from a recursive tree listing.
3509
- * A directory is a member if it contains a NODE.md blob.
3510
- */
3511
- function extractMemberDirs(treeEntries) {
3512
- const nodeMdPaths = /* @__PURE__ */ new Set();
3513
- for (const entry of treeEntries) if (entry.type === "blob" && entry.path.endsWith("/NODE.md")) nodeMdPaths.add(entry.path);
3514
- const memberDirs = [];
3515
- for (const entry of treeEntries) {
3516
- if (entry.type !== "tree") continue;
3517
- if (nodeMdPaths.has(`${entry.path}/NODE.md`)) memberDirs.push(entry.path);
3518
- }
3519
- return memberDirs.sort();
3520
- }
3521
- /**
3522
- * Fetch all members from a Context Tree repo via GitHub API.
3523
- * Uses 3 API calls:
3524
- * 1. GraphQL: get members/ tree OID
3525
- * 2. REST: recursive tree listing (scoped to members/ only)
3526
- * 3. GraphQL: batch-fetch all NODE.md contents via aliases
3527
- */
3528
- async function fetchMembers(repo, branch, token) {
3529
- const { owner, name } = parseRepo(repo);
3530
- if (!owner || !name) throw new Error(`Invalid repo format: "${repo}" — expected "owner/repo" or a GitHub URL`);
3531
- const treeOid = await fetchMembersTreeOid(owner, name, branch, token);
3532
- if (!treeOid) {
3533
- console.warn("[context-tree-sync] members/ directory not found in repo");
3534
- return [];
3535
- }
3536
- const memberDirs = extractMemberDirs(await fetchRecursiveTree(owner, name, treeOid, token));
3537
- if (memberDirs.length === 0) {
3538
- console.warn("[context-tree-sync] No member directories with NODE.md found");
3539
- return [];
3540
- }
3541
- const nameMap = /* @__PURE__ */ new Map();
3542
- for (const dir of memberDirs) {
3543
- const dirName = dir.split("/").pop() ?? dir;
3544
- const existing = nameMap.get(dirName);
3545
- if (existing) throw new Error(`[context-tree-sync] Duplicate member directory name '${dirName}' found at 'members/${existing}' and 'members/${dir}' — directory names must be unique across all levels under members/. Fix this in the Context Tree repo.`);
3546
- nameMap.set(dirName, dir);
3547
- }
3548
- const nodeContents = await batchFetchNodeMd(owner, name, branch, memberDirs, token);
3549
- return memberDirs.map((dir) => ({
3550
- name: dir.split("/").pop() ?? dir,
3551
- treePath: dir,
3552
- nodeContent: nodeContents.get(dir) ?? null
3553
- }));
3554
- }
3555
- /** Parse NODE.md frontmatter for agent metadata. */
3556
- function parseNodeMetadata(content) {
3557
- const match = /^---\n([\s\S]*?)\n---/.exec(content);
3558
- if (!match) return {
3559
- type: "autonomous_agent",
3560
- displayName: null,
3561
- delegateMention: null,
3562
- owners: [],
3563
- github: null
3564
- };
3565
- const frontmatter = match[1] ?? "";
3566
- const getValue = (key) => {
3567
- const lineMatch = new RegExp(`^${key}:\\s*(.+)$`, "m").exec(frontmatter);
3568
- return lineMatch ? lineMatch[1]?.trim().replace(/^["']|["']$/g, "") ?? null : null;
3569
- };
3570
- const ownersRaw = getValue("owners");
3571
- let owners = [];
3572
- if (ownersRaw) {
3573
- const listMatch = /^\[([^\]]*)\]$/.exec(ownersRaw);
3574
- if (listMatch?.[1]) owners = listMatch[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
3575
- }
3576
- return {
3577
- type: getValue("type") ?? "autonomous_agent",
3578
- displayName: getValue("display_name") ?? getValue("title") ?? getValue("name"),
3579
- delegateMention: getValue("delegate_mention"),
3580
- owners,
3581
- github: getValue("github")
3582
- };
3583
- }
3584
- /** Stored for the /status endpoint */
3585
- let _lastSyncResult;
3586
- function getLastGraphQLSyncResult() {
3587
- return _lastSyncResult;
3588
- }
3589
- /**
3590
- * Sync agents from a GitHub Context Tree repo via GraphQL.
3591
- *
3592
- * Lifecycle semantics:
3593
- * - Member in tree, not in DB → create (active)
3594
- * - Member in tree, in DB as active, fields changed → update
3595
- * - Member in tree, in DB as active, fields unchanged → unchanged
3596
- * - Member in tree, in DB as suspended → reactivate (set active)
3597
- * - Agent in DB as active, NOT in tree → suspend
3598
- */
3599
- async function syncFromGitHub(db, repo, branch, githubToken) {
3600
- const members = await fetchMembers(repo, branch, githubToken);
3601
- const memberNames = new Set(members.map((m) => m.name));
3602
- const result = {
3603
- created: 0,
3604
- updated: 0,
3605
- suspended: 0,
3606
- reactivated: 0,
3607
- unchanged: 0,
3608
- errors: 0,
3609
- syncedAt: (/* @__PURE__ */ new Date()).toISOString()
3610
- };
3611
- for (const member of members) try {
3612
- const meta = member.nodeContent ? parseNodeMetadata(member.nodeContent) : {
3613
- type: "autonomous_agent",
3614
- displayName: null,
3615
- delegateMention: null,
3616
- owners: [],
3617
- github: null
3618
- };
3619
- const metadataJson = JSON.stringify({
3620
- owners: meta.owners,
3621
- github: meta.github
3622
- });
3623
- const existing = await db.execute(sql`SELECT id, status, type, display_name, delegate_mention, tree_path, metadata FROM agents WHERE id = ${member.name}`);
3624
- if (existing.length === 0) {
3625
- await db.execute(sql`
3626
- INSERT INTO agents (id, type, display_name, delegate_mention, tree_path, status, inbox_id, metadata)
3627
- VALUES (${member.name}, ${meta.type}, ${meta.displayName}, ${meta.delegateMention}, ${member.treePath}, 'active', ${`inbox_${member.name}`}, ${metadataJson}::jsonb)
3628
- `);
3629
- result.created++;
3630
- } else {
3631
- const agent = existing[0];
3632
- const existingMeta = agent.metadata ?? {};
3633
- const mergedMeta = JSON.stringify({
3634
- ...existingMeta,
3635
- owners: meta.owners,
3636
- github: meta.github
3637
- });
3638
- if (agent.status === "suspended" || agent.status === "deleted") {
3639
- await db.execute(sql`
3640
- UPDATE agents SET status = 'active', type = ${meta.type}, display_name = ${meta.displayName}, delegate_mention = ${meta.delegateMention}, tree_path = ${member.treePath}, metadata = ${mergedMeta}::jsonb
3641
- WHERE id = ${member.name}
3642
- `);
3643
- result.reactivated++;
3644
- } 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)) {
3645
- await db.execute(sql`
3646
- UPDATE agents SET type = ${meta.type}, display_name = ${meta.displayName}, delegate_mention = ${meta.delegateMention}, tree_path = ${member.treePath}, metadata = ${mergedMeta}::jsonb
3647
- WHERE id = ${member.name}
3648
- `);
3649
- result.updated++;
3650
- } else result.unchanged++;
3651
- }
3652
- } catch (err) {
3653
- console.error(`[context-tree-sync] Failed to sync member "${member.name}" (path: members/${member.treePath}):`, err);
3654
- result.errors++;
3655
- }
3656
- try {
3657
- const activeAgents = await db.execute(sql`SELECT id FROM agents WHERE status = 'active' AND (metadata->>'managed')::boolean IS NOT TRUE`);
3658
- for (const row of activeAgents) {
3659
- const agent = row;
3660
- if (!memberNames.has(agent.id)) {
3661
- await db.execute(sql`UPDATE agents SET status = 'suspended' WHERE id = ${agent.id}`);
3662
- result.suspended++;
3663
- }
3664
- }
3665
- } catch (err) {
3666
- console.error("[context-tree-sync] Failed to suspend removed agents:", err);
3667
- result.errors++;
3668
- }
3669
- _lastSyncResult = result;
3670
- return result;
3671
- }
3672
- async function adminAgentSyncRoutes(app) {
3673
- app.post("/", async (_request, reply) => {
3674
- const { repo, branch } = app.config.contextTree;
3675
- const { token } = app.config.github;
3676
- try {
3677
- const result = await syncFromGitHub(app.db, repo, branch, token);
3678
- return reply.send({ summary: result });
3679
- } catch (error) {
3680
- const msg = error instanceof Error ? error.message : String(error);
3681
- app.log.error(error, "Context Tree sync failed");
3682
- return reply.status(502).send({ error: msg });
3683
- }
3684
- });
3685
- app.get("/status", async (_request, reply) => {
3686
- const lastSync = getLastGraphQLSyncResult();
3687
- return reply.send({ lastSync: lastSync ?? null });
3688
- });
3689
- }
3690
3080
  /** Agent online status. Tracked via WebSocket connections; stale entries are cleaned up using server_instances heartbeat. */
3691
3081
  const agentPresence = pgTable("agent_presence", {
3692
3082
  agentId: text("agent_id").primaryKey().references(() => agents.id, { onDelete: "cascade" }),
@@ -3722,6 +3112,8 @@ async function createAgent(db, data) {
3722
3112
  organizationId: data.organizationId ?? "default",
3723
3113
  type: data.type,
3724
3114
  displayName: data.displayName ?? null,
3115
+ delegateMention: data.delegateMention ?? null,
3116
+ profile: data.profile ?? null,
3725
3117
  status: "active",
3726
3118
  metadata: data.metadata ?? {},
3727
3119
  updatedAt: /* @__PURE__ */ new Date()
@@ -3734,6 +3126,8 @@ async function createAgent(db, data) {
3734
3126
  organizationId: data.organizationId ?? "default",
3735
3127
  type: data.type,
3736
3128
  displayName: data.displayName ?? null,
3129
+ delegateMention: data.delegateMention ?? null,
3130
+ profile: data.profile ?? null,
3737
3131
  inboxId,
3738
3132
  metadata: data.metadata ?? {}
3739
3133
  }).returning();
@@ -3745,16 +3139,18 @@ async function getAgent(db, id) {
3745
3139
  if (!agent) throw new NotFoundError(`Agent "${id}" not found`);
3746
3140
  return agent;
3747
3141
  }
3748
- async function listAgents(db, limit, cursor) {
3749
- const notDeleted = ne(agents.status, AGENT_STATUSES.DELETED);
3750
- const where = cursor ? and(notDeleted, lt(agents.createdAt, new Date(cursor))) : notDeleted;
3142
+ async function listAgents(db, limit, cursor, type) {
3143
+ const conditions = [ne(agents.status, AGENT_STATUSES.DELETED)];
3144
+ if (cursor) conditions.push(lt(agents.createdAt, new Date(cursor)));
3145
+ if (type) conditions.push(eq(agents.type, type));
3146
+ const where = and(...conditions);
3751
3147
  const rows = await db.select({
3752
3148
  id: agents.id,
3753
3149
  organizationId: agents.organizationId,
3754
3150
  type: agents.type,
3755
3151
  displayName: agents.displayName,
3756
3152
  delegateMention: agents.delegateMention,
3757
- treePath: agents.treePath,
3153
+ profile: agents.profile,
3758
3154
  inboxId: agents.inboxId,
3759
3155
  status: agents.status,
3760
3156
  metadata: agents.metadata,
@@ -3770,8 +3166,50 @@ async function listAgents(db, limit, cursor) {
3770
3166
  nextCursor: hasMore && last ? last.createdAt.toISOString() : null
3771
3167
  };
3772
3168
  }
3169
+ async function updateAgent(db, id, data) {
3170
+ const agent = await getAgent(db, id);
3171
+ const updates = { updatedAt: /* @__PURE__ */ new Date() };
3172
+ if (data.type !== void 0) updates.type = data.type;
3173
+ if (data.displayName !== void 0) updates.displayName = data.displayName;
3174
+ if (data.delegateMention !== void 0) updates.delegateMention = data.delegateMention;
3175
+ if (data.profile !== void 0) updates.profile = data.profile;
3176
+ if (data.metadata !== void 0) updates.metadata = data.metadata;
3177
+ const [updated] = await db.update(agents).set(updates).where(eq(agents.id, agent.id)).returning();
3178
+ if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
3179
+ return updated;
3180
+ }
3773
3181
  /**
3774
- * Delete an agent. Only allowed when status is "suspended" (removed from tree).
3182
+ * Reactivate a suspended agent.
3183
+ */
3184
+ async function reactivateAgent(db, id) {
3185
+ const [existing] = await db.select({
3186
+ id: agents.id,
3187
+ status: agents.status
3188
+ }).from(agents).where(eq(agents.id, id)).limit(1);
3189
+ if (!existing || existing.status === AGENT_STATUSES.DELETED) throw new NotFoundError(`Agent "${id}" not found`);
3190
+ if (existing.status !== AGENT_STATUSES.SUSPENDED) throw new BadRequestError("Only suspended agents can be reactivated.");
3191
+ const [agent] = await db.update(agents).set({
3192
+ status: AGENT_STATUSES.ACTIVE,
3193
+ updatedAt: /* @__PURE__ */ new Date()
3194
+ }).where(eq(agents.id, id)).returning();
3195
+ if (!agent) throw new Error("Unexpected: UPDATE RETURNING produced no row");
3196
+ return agent;
3197
+ }
3198
+ /**
3199
+ * Suspend an agent. Revokes all active tokens so the agent can no longer authenticate.
3200
+ */
3201
+ async function suspendAgent(db, id) {
3202
+ const [agent] = await db.update(agents).set({
3203
+ status: AGENT_STATUSES.SUSPENDED,
3204
+ updatedAt: /* @__PURE__ */ new Date()
3205
+ }).where(and(eq(agents.id, id), ne(agents.status, AGENT_STATUSES.DELETED))).returning();
3206
+ if (!agent) throw new NotFoundError(`Agent "${id}" not found`);
3207
+ await db.update(agentTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and(eq(agentTokens.agentId, id), isNull(agentTokens.revokedAt)));
3208
+ return agent;
3209
+ }
3210
+ /**
3211
+ * Delete an agent. Only allowed when status is "suspended".
3212
+ * Suspend the agent first to revoke tokens, then delete.
3775
3213
  */
3776
3214
  async function deleteAgent(db, id) {
3777
3215
  const [existing] = await db.select({
@@ -3779,7 +3217,7 @@ async function deleteAgent(db, id) {
3779
3217
  status: agents.status
3780
3218
  }).from(agents).where(eq(agents.id, id)).limit(1);
3781
3219
  if (!existing || existing.status === AGENT_STATUSES.DELETED) throw new NotFoundError(`Agent "${id}" not found`);
3782
- if (existing.status !== AGENT_STATUSES.SUSPENDED) throw new BadRequestError("Only suspended agents can be deleted. Active agents are managed by Context Tree.");
3220
+ if (existing.status !== AGENT_STATUSES.SUSPENDED) throw new BadRequestError("Only suspended agents can be deleted. Suspend the agent first.");
3783
3221
  const [agent] = await db.update(agents).set({
3784
3222
  status: AGENT_STATUSES.DELETED,
3785
3223
  updatedAt: /* @__PURE__ */ new Date()
@@ -3790,16 +3228,41 @@ async function deleteAgent(db, id) {
3790
3228
  if (!agent) throw new Error("Unexpected: UPDATE RETURNING produced no row");
3791
3229
  return agent;
3792
3230
  }
3793
- /**
3794
- * Bootstrap a token for an agent using GitHub identity.
3795
- * Only works when the agent has no active (non-revoked, non-expired) tokens.
3796
- */
3797
- async function bootstrapToken(db, agentId, githubUsername, tokenName) {
3798
- const agent = await getAgent(db, agentId);
3231
+ async function bootstrapToken(db, agentId, githubUsername, options) {
3232
+ let agent;
3233
+ try {
3234
+ agent = await getAgent(db, agentId);
3235
+ } catch (err) {
3236
+ if (err instanceof NotFoundError) {
3237
+ const metadata = {
3238
+ ...options?.metadata,
3239
+ owners: [githubUsername]
3240
+ };
3241
+ agent = await createAgent(db, {
3242
+ id: agentId,
3243
+ type: options?.type ?? "autonomous_agent",
3244
+ displayName: options?.displayName ?? agentId,
3245
+ delegateMention: options?.delegateMention,
3246
+ profile: options?.profile,
3247
+ metadata
3248
+ });
3249
+ } else throw err;
3250
+ }
3799
3251
  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}"`);
3800
3252
  const activeTokens = await db.select({ id: agentTokens.id }).from(agentTokens).where(and(eq(agentTokens.agentId, agentId), isNull(agentTokens.revokedAt)));
3801
3253
  if (activeTokens.length > 0) throw new ConflictError(`Agent "${agentId}" already has ${activeTokens.length} active token(s). Revoke all tokens first to re-bootstrap.`);
3802
- return createToken(db, agentId, { name: tokenName ?? "bootstrap" });
3254
+ return createToken(db, agentId, { name: options?.tokenName ?? "bootstrap" });
3255
+ }
3256
+ /**
3257
+ * Check if a GitHub user belongs to a specific organization.
3258
+ */
3259
+ async function checkGitHubOrgMembership(githubToken, org) {
3260
+ const res = await fetch(`https://api.github.com/user/orgs`, { headers: {
3261
+ Authorization: `Bearer ${githubToken}`,
3262
+ Accept: "application/vnd.github+json"
3263
+ } });
3264
+ if (!res.ok) return false;
3265
+ return (await res.json()).some((o) => o.login.toLowerCase() === org.toLowerCase());
3803
3266
  }
3804
3267
  async function createToken(db, agentId, data) {
3805
3268
  await getAgent(db, agentId);
@@ -4236,9 +3699,11 @@ function serializeDate$1(d) {
4236
3699
  return d ? d.toISOString() : null;
4237
3700
  }
4238
3701
  async function adminAgentRoutes(app) {
3702
+ const listAgentsFilterSchema = z.object({ type: agentTypeSchema.optional() });
4239
3703
  app.get("/", async (request) => {
4240
3704
  const query = paginationQuerySchema.parse(request.query);
4241
- const result = await listAgents(app.db, query.limit, query.cursor);
3705
+ const { type } = listAgentsFilterSchema.parse(request.query);
3706
+ const result = await listAgents(app.db, query.limit, query.cursor, type);
4242
3707
  return {
4243
3708
  items: result.items.map((a) => ({
4244
3709
  ...a,
@@ -4249,6 +3714,24 @@ async function adminAgentRoutes(app) {
4249
3714
  nextCursor: result.nextCursor
4250
3715
  };
4251
3716
  });
3717
+ app.post("/", async (request, reply) => {
3718
+ const body = createAgentSchema.parse(request.body);
3719
+ const agent = await createAgent(app.db, body);
3720
+ return reply.status(201).send({
3721
+ ...agent,
3722
+ createdAt: agent.createdAt.toISOString(),
3723
+ updatedAt: agent.updatedAt.toISOString()
3724
+ });
3725
+ });
3726
+ app.patch("/:agentId", async (request) => {
3727
+ const body = updateAgentSchema.parse(request.body);
3728
+ const agent = await updateAgent(app.db, request.params.agentId, body);
3729
+ return {
3730
+ ...agent,
3731
+ createdAt: agent.createdAt.toISOString(),
3732
+ updatedAt: agent.updatedAt.toISOString()
3733
+ };
3734
+ });
4252
3735
  app.get("/:agentId", async (request) => {
4253
3736
  const agent = await getAgent(app.db, request.params.agentId);
4254
3737
  return {
@@ -4288,6 +3771,22 @@ async function adminAgentRoutes(app) {
4288
3771
  await setOffline(app.db, agentId);
4289
3772
  return reply.status(200).send({ disconnected: wasConnected });
4290
3773
  });
3774
+ app.post("/:agentId/suspend", async (request) => {
3775
+ const agent = await suspendAgent(app.db, request.params.agentId);
3776
+ return {
3777
+ ...agent,
3778
+ createdAt: agent.createdAt.toISOString(),
3779
+ updatedAt: agent.updatedAt.toISOString()
3780
+ };
3781
+ });
3782
+ app.post("/:agentId/reactivate", async (request) => {
3783
+ const agent = await reactivateAgent(app.db, request.params.agentId);
3784
+ return {
3785
+ ...agent,
3786
+ createdAt: agent.createdAt.toISOString(),
3787
+ updatedAt: agent.updatedAt.toISOString()
3788
+ };
3789
+ });
4291
3790
  app.delete("/:agentId", async (request, reply) => {
4292
3791
  await deleteAgent(app.db, request.params.agentId);
4293
3792
  return reply.status(204).send();
@@ -4683,16 +4182,6 @@ async function agentChatRoutes(app) {
4683
4182
  return reply.status(204).send();
4684
4183
  });
4685
4184
  }
4686
- async function agentContextTreeRoutes(app) {
4687
- app.get("/", async (_request, reply) => {
4688
- const { repo, branch } = app.config.contextTree;
4689
- if (!repo) return reply.status(404).send({ error: "Context Tree not configured" });
4690
- return reply.send({
4691
- repo,
4692
- branch
4693
- });
4694
- });
4695
- }
4696
4185
  async function agentFeishuBotRoutes(app) {
4697
4186
  /**
4698
4187
  * PUT /agent/me/feishu-bot
@@ -4832,7 +4321,7 @@ async function pollInbox(db, inboxId, limit) {
4832
4321
  });
4833
4322
  });
4834
4323
  }
4835
- async function ackEntry$1(db, entryId, inboxId) {
4324
+ async function ackEntry$2(db, entryId, inboxId) {
4836
4325
  const [entry] = await db.update(inboxEntries).set({
4837
4326
  status: "acked",
4838
4327
  ackedAt: /* @__PURE__ */ new Date()
@@ -4874,7 +4363,7 @@ async function agentInboxRoutes(app) {
4874
4363
  app.post("/:entryId/ack", async (request, reply) => {
4875
4364
  const identity = requireAgent(request);
4876
4365
  const entryId = Number(request.params.entryId);
4877
- await ackEntry$1(app.db, entryId, identity.inboxId);
4366
+ await ackEntry$2(app.db, entryId, identity.inboxId);
4878
4367
  return reply.status(204).send();
4879
4368
  });
4880
4369
  app.post("/:entryId/renew", async (request, reply) => {
@@ -4976,18 +4465,37 @@ function agentWsRoutes(notifier, instanceId) {
4976
4465
  });
4977
4466
  };
4978
4467
  }
4468
+ async function bootstrapConfigRoutes(app) {
4469
+ /** Public endpoint — returns bootstrap prerequisites for CLI auto-discovery. */
4470
+ app.get("/config", async () => {
4471
+ return { allowedOrg: app.config.github.allowedOrg ?? null };
4472
+ });
4473
+ }
4979
4474
  async function bootstrapRoutes(app) {
4980
4475
  /**
4981
4476
  * POST /bootstrap/:agentId/token
4982
4477
  * GitHub identity → Agent token.
4478
+ * Auto-creates the agent if it does not exist.
4983
4479
  * Only works when the agent has no active tokens.
4984
4480
  */
4985
4481
  app.post("/:agentId/token", async (request, reply) => {
4986
4482
  const { agentId } = request.params;
4987
4483
  const githubUser = request.githubUser;
4988
4484
  if (!githubUser) throw new ForbiddenError("GitHub authentication required");
4485
+ const allowedOrg = app.config.github.allowedOrg;
4486
+ if (!allowedOrg) throw new ForbiddenError("FIRST_TREE_HUB_GITHUB_ALLOWED_ORG is not configured. Agent registration is disabled.");
4487
+ const githubToken = request.headers["x-github-token"];
4488
+ if (!githubToken || typeof githubToken !== "string") throw new ForbiddenError("Missing GitHub token for org membership check");
4489
+ if (!await checkGitHubOrgMembership(githubToken, allowedOrg)) throw new ForbiddenError(`GitHub user "${githubUser.username}" is not a member of organization "${allowedOrg}"`);
4989
4490
  const body = bootstrapTokenRequestSchema.parse(request.body ?? {});
4990
- const result = await bootstrapToken(app.db, agentId, githubUser.username, body.name);
4491
+ const result = await bootstrapToken(app.db, agentId, githubUser.username, {
4492
+ tokenName: body.name,
4493
+ type: body.type,
4494
+ displayName: body.displayName,
4495
+ delegateMention: body.delegateMention,
4496
+ profile: body.profile,
4497
+ metadata: body.metadata
4498
+ });
4991
4499
  return reply.status(201).send({
4992
4500
  id: result.id,
4993
4501
  agentId: result.agentId,
@@ -4999,7 +4507,7 @@ async function bootstrapRoutes(app) {
4999
4507
  });
5000
4508
  /**
5001
4509
  * GET /bootstrap/:agentId/status
5002
- * Check if an agent exists and its status (for polling after PR merge + sync).
4510
+ * Check if an agent exists and its status (for polling).
5003
4511
  */
5004
4512
  app.get("/:agentId/status", async (request) => {
5005
4513
  const { agentId } = request.params;
@@ -5024,11 +4532,9 @@ async function bootstrapRoutes(app) {
5024
4532
  async function contextTreeInfoRoutes(app) {
5025
4533
  /** Public endpoint — returns Context Tree repo metadata for CLI auto-discovery. */
5026
4534
  app.get("/info", async () => {
5027
- const { repo, branch } = app.config.contextTree;
5028
4535
  return {
5029
- repo,
5030
- branch,
5031
- lastSync: null
4536
+ repo: app.config.contextTree?.repo ?? null,
4537
+ branch: app.config.contextTree?.branch ?? null
5032
4538
  };
5033
4539
  });
5034
4540
  }
@@ -5647,7 +5153,7 @@ async function withoutProxy(fn) {
5647
5153
  for (const [key, val] of Object.entries(saved)) process.env[key] = val;
5648
5154
  }
5649
5155
  }
5650
- const OUTBOUND_BATCH_SIZE = 10;
5156
+ const OUTBOUND_BATCH_SIZE$1 = 10;
5651
5157
  /** Wrap an SDK API call with proxy bypass if needed. */
5652
5158
  function botApiCall(bot, fn) {
5653
5159
  return bot.bypassProxy ? withoutProxy(fn) : fn();
@@ -5822,6 +5328,8 @@ function parseEventData(appId, data) {
5822
5328
  const sender = data.sender;
5823
5329
  const message = data.message;
5824
5330
  if (!sender?.sender_id?.open_id || !message) return null;
5331
+ 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`);
5332
+ const resolvedSenderId = sender.sender_id.union_id ?? sender.sender_id.open_id;
5825
5333
  const eventId = data.event_id ?? `ws_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
5826
5334
  let parsedContent;
5827
5335
  try {
@@ -5838,7 +5346,8 @@ function parseEventData(appId, data) {
5838
5346
  eventId,
5839
5347
  platform: "feishu",
5840
5348
  appId,
5841
- senderId: sender.sender_id.open_id,
5349
+ senderId: resolvedSenderId,
5350
+ senderOpenId: sender.sender_id.open_id,
5842
5351
  senderType: sender.sender_type ?? "user",
5843
5352
  externalChannelId: message.chat_id ?? "",
5844
5353
  chatType: message.chat_type ?? "group",
@@ -6011,7 +5520,7 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
6011
5520
  JOIN adapter_agent_mappings aam ON a.id = aam.agent_id
6012
5521
  WHERE aam.platform = 'feishu' AND ie.status = 'pending'
6013
5522
  ORDER BY ie.created_at
6014
- LIMIT ${OUTBOUND_BATCH_SIZE}
5523
+ LIMIT ${OUTBOUND_BATCH_SIZE$1}
6015
5524
  FOR UPDATE SKIP LOCKED
6016
5525
  )
6017
5526
  RETURNING id, inbox_id, message_id, chat_id
@@ -6020,21 +5529,21 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
6020
5529
  for (const entry of claimed) try {
6021
5530
  const [msg] = await db.select().from(messages).where(eq(messages.id, entry.message_id)).limit(1);
6022
5531
  if (!msg) {
6023
- await ackEntry(db, entry.id);
5532
+ await ackEntry$1(db, entry.id);
6024
5533
  continue;
6025
5534
  }
6026
5535
  if (msg.metadata?.source === "feishu") {
6027
- await ackEntry(db, entry.id);
5536
+ await ackEntry$1(db, entry.id);
6028
5537
  continue;
6029
5538
  }
6030
5539
  const channelMapping = await findExternalChannelByChat(db, "feishu", entry.chat_id ?? msg.chatId);
6031
5540
  if (!channelMapping) {
6032
- await ackEntry(db, entry.id);
5541
+ await ackEntry$1(db, entry.id);
6033
5542
  continue;
6034
5543
  }
6035
5544
  const dedupKey = `${msg.id}:${channelMapping.externalChannelId}`;
6036
5545
  if (sentMessages.has(dedupKey)) {
6037
- await ackEntry(db, entry.id);
5546
+ await ackEntry$1(db, entry.id);
6038
5547
  continue;
6039
5548
  }
6040
5549
  const bot = findBotByAgentId(msg.senderId);
@@ -6043,7 +5552,7 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
6043
5552
  messageId: msg.id,
6044
5553
  senderId: msg.senderId
6045
5554
  }, "Outbound skip: sender has no feishu bot binding");
6046
- await ackEntry(db, entry.id);
5555
+ await ackEntry$1(db, entry.id);
6047
5556
  continue;
6048
5557
  }
6049
5558
  const { msgType, content } = formatForFeishu(msg.format, msg.content);
@@ -6063,7 +5572,7 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
6063
5572
  externalMessageId: externalMsgId,
6064
5573
  externalChannelId: channelMapping.externalChannelId
6065
5574
  });
6066
- await ackEntry(db, entry.id);
5575
+ await ackEntry$1(db, entry.id);
6067
5576
  bot.lastActiveAt = /* @__PURE__ */ new Date();
6068
5577
  sent++;
6069
5578
  } catch (err) {
@@ -6078,7 +5587,7 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
6078
5587
  errors: errorCount
6079
5588
  };
6080
5589
  }
6081
- async function ackEntry(db, entryId) {
5590
+ async function ackEntry$1(db, entryId) {
6082
5591
  await db.update(inboxEntries).set({
6083
5592
  status: "acked",
6084
5593
  ackedAt: /* @__PURE__ */ new Date()
@@ -6116,10 +5625,11 @@ function formatForFeishu(format, content) {
6116
5625
  content: JSON.stringify({ text })
6117
5626
  };
6118
5627
  }
6119
- function createBackgroundTasks(app, instanceId, adapterManager) {
5628
+ function createBackgroundTasks(app, instanceId, adapterManager, kaelRuntime) {
6120
5629
  let inboxTimer = null;
6121
5630
  let heartbeatTimer = null;
6122
5631
  let adapterOutboundTimer = null;
5632
+ let kaelOutboundTimer = null;
6123
5633
  return {
6124
5634
  start() {
6125
5635
  inboxTimer = setInterval(async () => {
@@ -6148,6 +5658,13 @@ function createBackgroundTasks(app, instanceId, adapterManager) {
6148
5658
  app.log.error(err, "Adapter outbound processing failed");
6149
5659
  }
6150
5660
  }, 5e3);
5661
+ if (kaelRuntime) kaelOutboundTimer = setInterval(async () => {
5662
+ try {
5663
+ await kaelRuntime.processOutbound();
5664
+ } catch (err) {
5665
+ app.log.error(err, "Kael outbound processing failed");
5666
+ }
5667
+ }, 5e3);
6151
5668
  heartbeatInstance(app.db, instanceId).catch((err) => {
6152
5669
  app.log.error(err, "Failed initial heartbeat");
6153
5670
  });
@@ -6165,9 +5682,195 @@ function createBackgroundTasks(app, instanceId, adapterManager) {
6165
5682
  clearInterval(adapterOutboundTimer);
6166
5683
  adapterOutboundTimer = null;
6167
5684
  }
5685
+ if (kaelOutboundTimer) {
5686
+ clearInterval(kaelOutboundTimer);
5687
+ kaelOutboundTimer = null;
5688
+ }
6168
5689
  }
6169
5690
  };
6170
5691
  }
5692
+ const OUTBOUND_BATCH_SIZE = 10;
5693
+ function createKaelRuntime(db, encryptionKey, kaelEndpoint, kaelApiKey, serverUrl, log) {
5694
+ const agentConfigs = /* @__PURE__ */ new Map();
5695
+ const inboxToConfig = /* @__PURE__ */ new Map();
5696
+ let aborted = false;
5697
+ return {
5698
+ async reload() {
5699
+ if (!encryptionKey) {
5700
+ log.warn("Encryption key not set — Kael runtime disabled");
5701
+ return;
5702
+ }
5703
+ if (!kaelEndpoint) {
5704
+ log.debug("KAEL_ENDPOINT not configured — Kael runtime idle");
5705
+ agentConfigs.clear();
5706
+ inboxToConfig.clear();
5707
+ return;
5708
+ }
5709
+ const configs = await db.select().from(adapterConfigs).where(and(eq(adapterConfigs.platform, "kael"), eq(adapterConfigs.status, "active")));
5710
+ const configAgentIds = configs.filter((c) => c.credentials).map((c) => c.agentId);
5711
+ 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'`) : [];
5712
+ const agentInboxMap = new Map(agentRows.map((a) => [a.id, a.inbox_id]));
5713
+ const seen = /* @__PURE__ */ new Set();
5714
+ for (const config of configs) {
5715
+ if (!config.credentials) continue;
5716
+ let creds;
5717
+ try {
5718
+ creds = decryptCredentials(config.credentials, encryptionKey);
5719
+ } catch (err) {
5720
+ log.error({
5721
+ configId: config.id,
5722
+ err
5723
+ }, "Failed to decrypt Kael adapter credentials");
5724
+ continue;
5725
+ }
5726
+ seen.add(config.agentId);
5727
+ const inboxId = agentInboxMap.get(config.agentId);
5728
+ if (!inboxId) {
5729
+ log.warn({
5730
+ configId: config.id,
5731
+ agentId: config.agentId
5732
+ }, "Kael config agent not found or inactive");
5733
+ continue;
5734
+ }
5735
+ const entry = {
5736
+ configId: config.id,
5737
+ agentId: config.agentId,
5738
+ inboxId,
5739
+ kaelUserId: creds.kaelUserId,
5740
+ kaelProjectId: creds.kaelProjectId,
5741
+ agentToken: creds.agentToken
5742
+ };
5743
+ agentConfigs.set(config.agentId, entry);
5744
+ inboxToConfig.set(inboxId, entry);
5745
+ log.info({
5746
+ configId: config.id,
5747
+ agentId: config.agentId
5748
+ }, "Loaded Kael adapter config");
5749
+ }
5750
+ for (const agentId of agentConfigs.keys()) if (!seen.has(agentId)) {
5751
+ const old = agentConfigs.get(agentId);
5752
+ if (old) inboxToConfig.delete(old.inboxId);
5753
+ agentConfigs.delete(agentId);
5754
+ log.info({ agentId }, "Removed inactive Kael adapter config");
5755
+ }
5756
+ },
5757
+ async processOutbound() {
5758
+ if (agentConfigs.size === 0 || !kaelEndpoint || aborted) return {
5759
+ sent: 0,
5760
+ errors: 0
5761
+ };
5762
+ let sent = 0;
5763
+ let errorCount = 0;
5764
+ try {
5765
+ const agentIds = [...agentConfigs.keys()];
5766
+ const claimed = await db.execute(sql`
5767
+ UPDATE inbox_entries
5768
+ SET status = 'delivered', delivered_at = NOW()
5769
+ WHERE id IN (
5770
+ SELECT ie.id FROM inbox_entries ie
5771
+ JOIN agents a ON ie.inbox_id = a.inbox_id
5772
+ JOIN adapter_configs ac ON a.id = ac.agent_id
5773
+ WHERE ac.platform = 'kael' AND ac.status = 'active'
5774
+ AND ie.status = 'pending'
5775
+ AND a.id IN (${sql.join(agentIds.map((id) => sql`${id}`), sql`, `)})
5776
+ ORDER BY ie.created_at
5777
+ LIMIT ${OUTBOUND_BATCH_SIZE}
5778
+ FOR UPDATE OF ie SKIP LOCKED
5779
+ )
5780
+ RETURNING id, inbox_id, message_id, chat_id
5781
+ `);
5782
+ for (const entry of claimed) try {
5783
+ const [msg] = await db.select().from(messages).where(eq(messages.id, entry.message_id)).limit(1);
5784
+ if (!msg) {
5785
+ await ackEntry(db, entry.id);
5786
+ continue;
5787
+ }
5788
+ const config = inboxToConfig.get(entry.inbox_id);
5789
+ if (!config) {
5790
+ await ackEntry(db, entry.id);
5791
+ continue;
5792
+ }
5793
+ const messageContent = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
5794
+ const payload = {
5795
+ hub_chat_id: entry.chat_id ?? msg.chatId,
5796
+ hub_agent_id: config.agentId,
5797
+ hub_server_url: serverUrl,
5798
+ hub_agent_token: config.agentToken,
5799
+ user_id: config.kaelUserId,
5800
+ project_id: config.kaelProjectId,
5801
+ message: messageContent,
5802
+ sender_id: msg.senderId,
5803
+ format: msg.format
5804
+ };
5805
+ const response = await fetch(`${kaelEndpoint}/api/v1/hub/messages`, {
5806
+ method: "POST",
5807
+ headers: {
5808
+ "Content-Type": "application/json",
5809
+ ...kaelApiKey ? { "X-Internal-API-Key": kaelApiKey } : {}
5810
+ },
5811
+ body: JSON.stringify(payload)
5812
+ });
5813
+ if (!response.ok) {
5814
+ const body = await response.text().catch(() => "");
5815
+ log.error({
5816
+ entryId: entry.id,
5817
+ status: response.status,
5818
+ body
5819
+ }, "Kael API rejected outbound message");
5820
+ await nackEntry(db, entry.id);
5821
+ errorCount++;
5822
+ continue;
5823
+ }
5824
+ await ackEntry(db, entry.id);
5825
+ sent++;
5826
+ } catch (err) {
5827
+ log.error({
5828
+ entryId: entry.id,
5829
+ err
5830
+ }, "Failed to send outbound Kael message");
5831
+ await nackEntry(db, entry.id).catch((nackErr) => {
5832
+ log.error({
5833
+ entryId: entry.id,
5834
+ err: nackErr
5835
+ }, "Failed to NACK entry");
5836
+ });
5837
+ errorCount++;
5838
+ }
5839
+ } catch (err) {
5840
+ log.error({ err }, "Kael outbound processing error");
5841
+ return {
5842
+ sent: 0,
5843
+ errors: 1
5844
+ };
5845
+ }
5846
+ return {
5847
+ sent,
5848
+ errors: errorCount
5849
+ };
5850
+ },
5851
+ shutdown() {
5852
+ aborted = true;
5853
+ agentConfigs.clear();
5854
+ inboxToConfig.clear();
5855
+ }
5856
+ };
5857
+ }
5858
+ async function ackEntry(db, entryId) {
5859
+ await db.update(inboxEntries).set({
5860
+ status: "acked",
5861
+ ackedAt: /* @__PURE__ */ new Date()
5862
+ }).where(eq(inboxEntries.id, entryId));
5863
+ }
5864
+ const MAX_RETRY_COUNT = 3;
5865
+ async function nackEntry(db, entryId) {
5866
+ await db.execute(sql`
5867
+ UPDATE inbox_entries
5868
+ SET
5869
+ status = CASE WHEN retry_count >= ${MAX_RETRY_COUNT} THEN 'failed' ELSE 'pending' END,
5870
+ retry_count = retry_count + 1
5871
+ WHERE id = ${entryId}
5872
+ `);
5873
+ }
6171
5874
  async function buildApp(config) {
6172
5875
  const app = Fastify({ logger: config.logger ?? true });
6173
5876
  const db = connectDatabase(config.database.url);
@@ -6204,6 +5907,7 @@ async function buildApp(config) {
6204
5907
  await api.register(githubWebhookRoutes, { prefix: "/webhooks" });
6205
5908
  await api.register(adminAuthRoutes, { prefix: "/admin/auth" });
6206
5909
  await api.register(contextTreeInfoRoutes, { prefix: "/context-tree" });
5910
+ await api.register(bootstrapConfigRoutes, { prefix: "/bootstrap" });
6207
5911
  await api.register(async (bootstrapApp) => {
6208
5912
  bootstrapApp.addHook("onRequest", githubAuth);
6209
5913
  await bootstrapApp.register(bootstrapRoutes);
@@ -6212,10 +5916,6 @@ async function buildApp(config) {
6212
5916
  adminApp.addHook("onRequest", adminAuth);
6213
5917
  await adminApp.register(adminAgentRoutes);
6214
5918
  }, { prefix: "/admin/agents" });
6215
- await api.register(async (adminApp) => {
6216
- adminApp.addHook("onRequest", adminAuth);
6217
- await adminApp.register(adminAgentSyncRoutes);
6218
- }, { prefix: "/admin/agents/sync" });
6219
5919
  await api.register(async (adminApp) => {
6220
5920
  adminApp.addHook("onRequest", adminAuth);
6221
5921
  await adminApp.register(adminSystemConfigRoutes);
@@ -6251,7 +5951,6 @@ async function buildApp(config) {
6251
5951
  await agentApp.register(agentMessageRoutes, { prefix: "/chats" });
6252
5952
  await agentApp.register(agentSendToAgentRoutes, { prefix: "/agents" });
6253
5953
  await agentApp.register(agentInboxRoutes, { prefix: "/inbox" });
6254
- await agentApp.register(agentContextTreeRoutes, { prefix: "/context-tree" });
6255
5954
  await agentApp.register(agentFeishuBotRoutes);
6256
5955
  await agentApp.register(agentFeishuUserRoutes, { prefix: "/delegated" });
6257
5956
  await agentApp.register(agentWsRoutes(notifier, config.instanceId), { prefix: "/ws" });
@@ -6274,31 +5973,24 @@ async function buildApp(config) {
6274
5973
  app.decorate("notifier", notifier);
6275
5974
  const adapterManager = createAdapterManager(db, config.secrets.encryptionKey, app.log, notifier);
6276
5975
  app.decorate("adapterManager", adapterManager);
6277
- const backgroundTasks = createBackgroundTasks(app, config.instanceId, adapterManager);
5976
+ const kaelRuntime = config.kael?.endpoint ? createKaelRuntime(db, config.secrets.encryptionKey, config.kael.endpoint, config.kael.apiKey, config.kael.hubPublicUrl, app.log) : void 0;
5977
+ const backgroundTasks = createBackgroundTasks(app, config.instanceId, adapterManager, kaelRuntime);
6278
5978
  notifier.onConfigChange((configType) => {
6279
- if (configType === "adapter_configs") adapterManager.reload().catch((err) => app.log.error(err, "Adapter hot-reload failed (PG NOTIFY)"));
5979
+ if (configType === "adapter_configs") {
5980
+ adapterManager.reload().catch((err) => app.log.error(err, "Adapter hot-reload failed (PG NOTIFY)"));
5981
+ kaelRuntime?.reload().catch((err) => app.log.error(err, "Kael hot-reload failed (PG NOTIFY)"));
5982
+ }
6280
5983
  });
6281
5984
  app.addHook("onReady", async () => {
6282
5985
  await notifier.start();
6283
5986
  backgroundTasks.start();
6284
5987
  await adapterManager.reload();
6285
- const { repo: ctRepo, branch: ctBranch, syncInterval } = config.contextTree;
6286
- const { token: ghToken } = config.github;
6287
- try {
6288
- const report = await syncFromGitHub(db, ctRepo, ctBranch, ghToken);
6289
- app.log.info(`Context Tree sync: created=${report.created} updated=${report.updated} unchanged=${report.unchanged} errors=${report.errors}`);
6290
- } catch (err) {
6291
- app.log.error(err, "Initial Context Tree sync failed");
6292
- }
6293
- const intervalMs = syncInterval * 1e3;
6294
- const timer = setInterval(() => {
6295
- syncFromGitHub(db, ctRepo, ctBranch, ghToken).catch((err) => app.log.error(err, "Periodic Context Tree sync failed"));
6296
- }, intervalMs);
6297
- app.addHook("onClose", async () => clearInterval(timer));
5988
+ await kaelRuntime?.reload();
6298
5989
  });
6299
5990
  app.addHook("onClose", async () => {
6300
5991
  backgroundTasks.stop();
6301
5992
  adapterManager.shutdown();
5993
+ kaelRuntime?.shutdown();
6302
5994
  await notifier.stop();
6303
5995
  await listenClient.end();
6304
5996
  });
@@ -6413,4 +6105,4 @@ function resolveWebDist() {
6413
6105
  } catch {}
6414
6106
  }
6415
6107
  //#endregion
6416
- export { ClientRuntime as A, checkWebSocket as C, ensurePostgres as D, status as E, SessionRegistry as F, cleanWorkspaces as I, hasAdminUser as M, FirstTreeHubSDK as N, isDockerAvailable as O, SdkError as P, checkServerReachable as S, blank as T, checkDocker as _, formatCheckReport as a, checkServerConfig as b, onboardContinue as c, runMigrations as d, checkAgentConfigs as f, checkDatabase as g, checkContextTreeRepo as h, promptMissingFields as i, createAdminUser$1 as j, stopPostgres as k, onboardCreate as l, checkClientConfig as m, isInteractive as n, loadOnboardState as o, checkAgentTokens as p, promptAddAgent as r, onboardCheck as s, startServer as t, saveOnboardState as u, checkGitHubToken as v, printResults as w, checkServerHealth as x, checkNodeVersion as y };
6108
+ export { createAdminUser$1 as A, printResults as C, isDockerAvailable as D, ensurePostgres as E, cleanWorkspaces as F, FirstTreeHubSDK as M, SdkError as N, stopPostgres as O, SessionRegistry as P, checkWebSocket as S, status as T, checkGitHubToken as _, formatCheckReport as a, checkServerHealth as b, onboardCreate as c, checkAgentConfigs as d, checkAgentTokens as f, checkDocker as g, checkDatabase as h, promptMissingFields as i, hasAdminUser as j, ClientRuntime as k, saveOnboardState as l, checkContextTreeRepo as m, isInteractive as n, loadOnboardState as o, checkClientConfig as p, promptAddAgent as r, onboardCheck as s, startServer as t, runMigrations as u, checkNodeVersion as v, blank as w, checkServerReachable as x, checkServerConfig as y };