@agent-team-foundation/first-tree-hub 0.3.5 → 0.5.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 { 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";
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-BU_7B03u.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";
@@ -45,18 +45,19 @@ var FirstTreeHubSDK = class {
45
45
  async register() {
46
46
  const agent = await this.requestJson("/api/v1/agent/me");
47
47
  return {
48
- agentId: agent.id,
48
+ agentId: agent.uuid,
49
49
  inboxId: agent.inboxId,
50
50
  status: agent.status,
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) {
@@ -78,8 +79,8 @@ var FirstTreeHubSDK = class {
78
79
  });
79
80
  }
80
81
  /** Send a direct message to another agent. */
81
- async sendToAgent(agentId, data) {
82
- return this.requestJson(`/api/v1/agent/agents/${agentId}/messages`, {
82
+ async sendToAgent(agentName, data) {
83
+ return this.requestJson(`/api/v1/agent/agents/${agentName}/messages`, {
83
84
  method: "POST",
84
85
  body: JSON.stringify(data)
85
86
  });
@@ -433,27 +434,23 @@ 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({
@@ -497,14 +494,13 @@ function bootstrapWorkspace(options) {
497
494
  contextTreePath
498
495
  };
499
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");
500
498
  if (contextTreePath) {
501
- const selfNodePath = join(contextTreePath, "members", identity.agentId, "NODE.md");
502
- if (existsSync(selfNodePath)) copyFileSync(selfNodePath, join(contextDir, "self.md"));
503
499
  const agentMdPath = join(contextTreePath, "AGENT.md");
504
500
  if (existsSync(agentMdPath)) copyFileSync(agentMdPath, join(contextDir, "agent-instructions.md"));
505
501
  const rootNodePath = join(contextTreePath, "NODE.md");
506
502
  if (existsSync(rootNodePath)) copyFileSync(rootNodePath, join(contextDir, "domain-map.md"));
507
- } 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
+ }
508
504
  writeFileSync(join(agentDir, "tools.md"), generateToolsDoc(), "utf-8");
509
505
  }
510
506
  function generateToolsDoc() {
@@ -842,10 +838,9 @@ const createClaudeCodeHandler = (config) => {
842
838
  /**
843
839
  * Generate a CLAUDE.md file from .agent/ bootstrap data.
844
840
  *
845
- * Layered Bootstrap:
846
- * Layer 1 (always): Agent identity + member profile + AGENT.md operating instructions
847
- * Layer 2 (if available): Organization domain map from root NODE.md
848
- * 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
849
844
  */
850
845
  function generateClaudeMd(workspacePath, identity, contextTreePath) {
851
846
  const sections = [];
@@ -857,25 +852,18 @@ function generateClaudeMd(workspacePath, identity, contextTreePath) {
857
852
  if (existsSync(selfMdPath)) {
858
853
  const selfContent = readFileSync(selfMdPath, "utf-8");
859
854
  sections.push(`## Your Profile\n\n${selfContent}\n`);
860
- } else sections.push("## Your Profile\n\nNo member profile available. Your responsibilities are not loaded from the Context Tree.\n");
855
+ }
861
856
  const agentInstructionsPath = join(contextDir, "agent-instructions.md");
862
857
  if (existsSync(agentInstructionsPath)) {
863
858
  const instructions = readFileSync(agentInstructionsPath, "utf-8");
864
- sections.push(`## Context Tree Operating Instructions\n\n${instructions}\n`);
865
- } 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
+ }
866
861
  const domainMapPath = join(contextDir, "domain-map.md");
867
862
  if (existsSync(domainMapPath)) {
868
863
  const domainMap = readFileSync(domainMapPath, "utf-8");
869
864
  sections.push(`## Organization Domain Map\n\n${domainMap}\n`);
870
865
  }
871
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`);
872
- else {
873
- const degradedPath = join(contextDir, "degraded.md");
874
- if (existsSync(degradedPath)) {
875
- const degradedMsg = readFileSync(degradedPath, "utf-8");
876
- sections.push(`## Context Tree Location\n\nWARNING: ${degradedMsg}\nYou can still use the SDK tools below, but you lack organizational context for decisions.\n`);
877
- }
878
- }
879
867
  const toolsPath = join(workspacePath, ".agent", "tools.md");
880
868
  if (existsSync(toolsPath)) {
881
869
  const toolsContent = readFileSync(toolsPath, "utf-8");
@@ -1299,6 +1287,7 @@ var AgentSlot = class {
1299
1287
  displayName: agent.displayName,
1300
1288
  type: agent.type,
1301
1289
  delegateMention: agent.delegateMention,
1290
+ profile: agent.profile,
1302
1291
  metadata: agent.metadata
1303
1292
  },
1304
1293
  sdk: this.connection.sdk,
@@ -2058,14 +2047,32 @@ async function onboardCheck(args) {
2058
2047
  key: "server_reachable",
2059
2048
  label: "Server reachable",
2060
2049
  status: res.ok ? "ok" : "error",
2061
- value: res.ok ? "yes" : `HTTP ${res.status}`
2050
+ value: res.ok ? "healthy" : `HTTP ${res.status}`
2062
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 {}
2063
2070
  } catch {
2064
2071
  items.push({
2065
2072
  key: "server_reachable",
2066
2073
  label: "Server reachable",
2067
2074
  status: "error",
2068
- value: "no"
2075
+ hint: "Cannot connect to server"
2069
2076
  });
2070
2077
  }
2071
2078
  } catch {
@@ -2073,126 +2080,32 @@ async function onboardCheck(args) {
2073
2080
  key: "server",
2074
2081
  label: "Server URL",
2075
2082
  status: "missing_required",
2076
- hint: "--server <url> or FIRST_TREE_HUB_SERVER"
2083
+ hint: "Provide via --server, FIRST_TREE_HUB_SERVER, or config"
2077
2084
  });
2078
2085
  }
2079
- const repoPath = await resolveContextTreeRepo(args.server);
2080
- if (repoPath) items.push({
2081
- key: "repo",
2082
- label: "Context Tree repo",
2083
- status: "ok",
2084
- value: repoPath
2085
- });
2086
- else {
2087
- const serverAvailable = items.some((i) => i.key === "server" && i.status === "ok");
2088
- items.push({
2089
- key: "repo",
2090
- label: "Context Tree repo",
2091
- status: "missing_required",
2092
- hint: serverAvailable ? "auto-clone failed (check server Context Tree config and gh auth)" : "configure --server first (repo will be auto-cloned from server)"
2093
- });
2094
- }
2095
- items.push(args.id ? {
2086
+ if (args.id) items.push({
2096
2087
  key: "id",
2097
- label: "id",
2088
+ label: "Agent ID",
2098
2089
  status: "ok",
2099
2090
  value: args.id
2100
- } : {
2091
+ });
2092
+ else items.push({
2101
2093
  key: "id",
2102
- label: "id",
2094
+ label: "Agent ID",
2103
2095
  status: "missing_required",
2104
- hint: "Member directory name"
2096
+ hint: "Provide via --id"
2105
2097
  });
2106
- items.push(args.type ? {
2098
+ if (args.type) items.push({
2107
2099
  key: "type",
2108
- label: "type",
2100
+ label: "Agent type",
2109
2101
  status: "ok",
2110
2102
  value: args.type
2111
- } : {
2112
- key: "type",
2113
- label: "type",
2114
- status: "missing_required",
2115
- hint: "human | personal_assistant | autonomous_agent"
2116
2103
  });
2117
- items.push(args.role ? {
2118
- key: "role",
2119
- label: "role",
2120
- status: "ok",
2121
- value: args.role
2122
- } : {
2123
- key: "role",
2124
- label: "role",
2125
- status: "missing_required",
2126
- hint: "e.g. \"Engineer\""
2127
- });
2128
- items.push(args.domains ? {
2129
- key: "domains",
2130
- label: "domains",
2131
- status: "ok",
2132
- value: args.domains
2133
- } : {
2134
- key: "domains",
2135
- label: "domains",
2136
- status: "missing_required",
2137
- hint: "Comma-separated, e.g. \"backend,infra\""
2138
- });
2139
- items.push(args.displayName ? {
2140
- key: "display_name",
2141
- label: "display-name",
2142
- status: "ok",
2143
- value: args.displayName
2144
- } : {
2145
- key: "display_name",
2146
- label: "display-name",
2147
- status: "missing_optional",
2148
- hint: `defaults to "${args.id ?? ""}"`
2149
- });
2150
- if (args.type === "human") items.push(args.assistant ? {
2151
- key: "assistant",
2152
- label: "assistant",
2153
- status: "ok",
2154
- value: args.assistant
2155
- } : {
2156
- key: "assistant",
2157
- label: "assistant",
2158
- status: "missing_optional",
2159
- hint: "Also create a personal_assistant"
2160
- });
2161
- if (args.type !== "human" || args.assistant) items.push(args.feishuBotAppId ? {
2162
- key: "feishu_bot",
2163
- label: "feishu-bot-app-id",
2164
- status: "ok",
2165
- value: args.feishuBotAppId
2166
- } : {
2167
- key: "feishu_bot",
2168
- label: "feishu-bot-app-id",
2169
- status: "missing_optional",
2170
- hint: "Feishu bot App ID"
2171
- });
2172
- if (args.id && repoPath) if (existsSync(join(repoPath, "members", args.id))) try {
2173
- execSync(`git ls-files --error-unmatch members/${args.id}/NODE.md`, {
2174
- cwd: repoPath,
2175
- stdio: "pipe"
2176
- });
2177
- items.push({
2178
- key: "conflict",
2179
- label: `ID "${args.id}" availability`,
2180
- status: "warning",
2181
- value: "already exists (will overwrite)"
2182
- });
2183
- } catch {
2184
- items.push({
2185
- key: "conflict",
2186
- label: `ID "${args.id}" availability`,
2187
- status: "ok",
2188
- value: "resuming (local files from previous run)"
2189
- });
2190
- }
2191
2104
  else items.push({
2192
- key: "conflict",
2193
- label: `ID "${args.id}" availability`,
2194
- status: "ok",
2195
- value: "available"
2105
+ key: "type",
2106
+ label: "Agent type",
2107
+ status: "missing_required",
2108
+ hint: "Provide via --type"
2196
2109
  });
2197
2110
  return items;
2198
2111
  }
@@ -2207,330 +2120,74 @@ function formatCheckReport(items) {
2207
2120
  return lines.join("\n");
2208
2121
  }
2209
2122
  async function onboardCreate(args) {
2210
- const repoPath = await resolveContextTreeRepo(args.server);
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}`);
2213
- const ghUsername = getGitHubUsername();
2214
- const githubField = args.type === "human" ? ghUsername : null;
2215
- const humanNodePath = join(repoPath, "members", args.id, "NODE.md");
2216
- if (existsSync(humanNodePath) && isTrackedByGit(repoPath, join("members", args.id, "NODE.md"))) {
2217
- process.stderr.write(`Member "${args.id}" already exists, skipping NODE.md creation.\n`);
2218
- if (args.assistant) {
2219
- const existingContent = readFileSync(humanNodePath, "utf-8");
2220
- if (!existingContent.includes("delegate_mention")) {
2221
- writeFileSync(humanNodePath, existingContent.replace(/^(---\n[\s\S]*?)(---)/m, `$1delegate_mention: ${args.assistant}\n$2`));
2222
- process.stderr.write(`Updated delegate_mention → ${args.assistant}\n`);
2223
- }
2224
- }
2225
- } else createMemberNodeMd(repoPath, {
2226
- id: args.id,
2227
- type: args.type,
2228
- displayName: args.displayName ?? args.id,
2229
- role: args.role,
2230
- domains: args.domains.split(",").map((d) => d.trim()),
2231
- owner: ghUsername,
2232
- github: githubField,
2233
- delegateMention: args.assistant ?? args.delegateMention ?? null
2234
- });
2235
- 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`);
2236
- else createMemberNodeMd(repoPath, {
2237
- parentPath: join("members", args.id),
2238
- id: args.assistant,
2239
- type: "personal_assistant",
2240
- displayName: args.assistant,
2241
- role: `Personal Assistant to ${args.id}`,
2242
- domains: ["message triage", "task coordination"],
2243
- owner: ghUsername,
2244
- github: null,
2245
- delegateMention: null
2246
- });
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;
2247
2130
  try {
2248
- execSync("npx -y first-tree verify", {
2249
- cwd: repoPath,
2250
- stdio: "pipe"
2251
- });
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;
2252
2139
  } catch (err) {
2253
- const stderr = err instanceof Error && "stderr" in err ? err.stderr.toString() : "";
2254
- const stdout = err instanceof Error && "stdout" in err ? err.stdout.toString() : "";
2255
- const output = stderr || stdout || String(err);
2256
- 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);
2257
- throw new Error(`Verification failed:\n${output}`);
2258
- }
2259
- const baseBranch = `onboard/${args.id}`;
2260
- let branch = baseBranch;
2261
- const branchExists = (name) => {
2262
- try {
2263
- execSync(`git rev-parse --verify ${name}`, {
2264
- cwd: repoPath,
2265
- stdio: "pipe"
2266
- });
2267
- return true;
2268
- } catch {
2269
- return false;
2270
- }
2271
- };
2272
- if (branchExists(branch)) branch = `${baseBranch}-${Date.now().toString(36)}`;
2273
- try {
2274
- execSync("git checkout main", {
2275
- cwd: repoPath,
2276
- stdio: "pipe"
2277
- });
2278
- } catch {
2279
- try {
2280
- execSync("git checkout master", {
2281
- cwd: repoPath,
2282
- stdio: "pipe"
2283
- });
2284
- } catch {}
2285
- }
2286
- execSync(`git checkout -b ${branch}`, {
2287
- cwd: repoPath,
2288
- stdio: "pipe"
2289
- });
2290
- execSync(`git add members/${args.id}`, {
2291
- cwd: repoPath,
2292
- stdio: "pipe"
2293
- });
2294
- execFileSync("git", [
2295
- "commit",
2296
- "-m",
2297
- args.assistant ? `feat: onboard ${args.id} + ${args.assistant}` : `feat: onboard ${args.id}`
2298
- ], {
2299
- cwd: repoPath,
2300
- stdio: "pipe"
2301
- });
2302
- const pushToken = execSync("gh auth token", {
2303
- encoding: "utf-8",
2304
- stdio: "pipe"
2305
- }).trim();
2306
- const cleanRemote = execSync("git remote get-url origin", {
2307
- cwd: repoPath,
2308
- encoding: "utf-8",
2309
- stdio: "pipe"
2310
- }).trim();
2311
- execSync(`git remote set-url origin "${cleanRemote.replace("https://github.com/", `https://x-access-token:${pushToken}@github.com/`)}"`, {
2312
- cwd: repoPath,
2313
- stdio: "pipe"
2314
- });
2315
- try {
2316
- execSync(`git push -u origin ${branch}`, {
2317
- cwd: repoPath,
2318
- stdio: "pipe"
2319
- });
2320
- } finally {
2321
- execSync(`git remote set-url origin "${cleanRemote}"`, {
2322
- cwd: repoPath,
2323
- stdio: "pipe"
2324
- });
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;
2325
2143
  }
2326
- const prOutput = execSync(`gh pr create --title "${args.assistant ? `Onboard ${args.id} + assistant` : `Onboard ${args.id}`}" --body "Automated onboard via first-tree-hub CLI"`, {
2327
- cwd: repoPath,
2328
- encoding: "utf-8"
2329
- }).trim();
2330
- const state = {
2331
- args,
2332
- branch,
2333
- prUrl: prOutput
2334
- };
2335
- mkdirSync(DEFAULT_HOME_DIR$1, { recursive: true });
2336
- writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
2337
- return { prUrl: prOutput };
2338
- }
2339
- async function onboardContinue(args) {
2340
- let state = null;
2341
- try {
2342
- state = JSON.parse(readFileSync(STATE_FILE, "utf-8"));
2343
- } catch {}
2344
- if (!state && !args.id) throw new Error("No onboard in progress. Run 'first-tree-hub onboard' first to start a new onboard.");
2345
- const mergedArgs = {
2346
- ...state?.args,
2347
- ...stripUndefined(args)
2348
- };
2349
- const serverUrl = resolveServerUrl(mergedArgs.server).replace(/\/+$/, "");
2350
- const agentToBootstrap = mergedArgs.assistant ?? mergedArgs.id;
2351
- if (!agentToBootstrap) throw new Error("Cannot determine which agent to bootstrap. Provide --id or run onboard first.");
2352
- if (!mergedArgs.id) throw new Error("Cannot determine member ID. Provide --id or run onboard first.");
2353
- process.stderr.write(`Waiting for agent "${agentToBootstrap}" to be synced...\n`);
2354
- let synced = false;
2355
- 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`);
2356
2147
  try {
2357
- const status = await checkBootstrapStatus(serverUrl, agentToBootstrap);
2358
- if (status.exists && status.status === "active") {
2359
- synced = true;
2360
- break;
2361
- }
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`);
2362
2158
  } catch (err) {
2363
- 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`);
2364
2161
  }
2365
- await sleep(2e3);
2366
- }
2367
- if (!synced) throw new Error(`Agent "${agentToBootstrap}" not found after 60s. Trigger sync manually or wait for auto-sync.`);
2368
- process.stderr.write(`Bootstrapping token for "${agentToBootstrap}"...\n`);
2369
- let token;
2370
- try {
2371
- token = (await bootstrapToken$1(serverUrl, agentToBootstrap, { saveTo: "agent" })).token;
2372
- } catch (err) {
2373
- const msg = err instanceof Error ? err.message : String(err);
2374
- 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:
2375
- first-tree-hub onboard --continue`);
2376
- throw err;
2377
2162
  }
2163
+ const agentToBootstrap = args.assistant ?? args.id;
2378
2164
  process.stderr.write(`Token saved to ${DEFAULT_HOME_DIR$1}/config/agents/${agentToBootstrap}/agent.yaml\n`);
2379
- if (mergedArgs.feishuBotAppId && mergedArgs.feishuBotAppSecret) {
2165
+ if (args.feishuBotAppId && args.feishuBotAppSecret) {
2380
2166
  const { bindFeishuBot } = await import("./feishu-Y4m2zFc3.mjs").then((n) => n.r);
2381
2167
  process.stderr.write("Binding Feishu bot...\n");
2382
- await bindFeishuBot(serverUrl, token, mergedArgs.feishuBotAppId, mergedArgs.feishuBotAppSecret);
2168
+ await bindFeishuBot(serverUrl, token, args.feishuBotAppId, args.feishuBotAppSecret);
2383
2169
  process.stderr.write("Feishu bot bound.\n");
2384
2170
  }
2171
+ setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", serverUrl);
2385
2172
  try {
2386
2173
  const { unlinkSync } = await import("node:fs");
2387
2174
  unlinkSync(STATE_FILE);
2388
2175
  } catch {}
2389
- const typeLabel = mergedArgs.type === "human" ? "Human" : mergedArgs.type === "autonomous_agent" ? "Agent" : "Assistant";
2176
+ const typeLabel = args.type === "human" ? "Human" : args.type === "autonomous_agent" ? "Agent" : "Assistant";
2390
2177
  process.stderr.write("\n✅ Onboard complete!\n\n");
2391
- process.stderr.write(` ${typeLabel}:${" ".repeat(Math.max(1, 10 - typeLabel.length))}${mergedArgs.id}\n`);
2392
- if (mergedArgs.assistant) process.stderr.write(` Assistant: ${mergedArgs.assistant}\n`);
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`);
2393
2180
  process.stderr.write(` Token: ${DEFAULT_HOME_DIR$1}/config/agents/${agentToBootstrap}/agent.yaml\n`);
2394
- if (mergedArgs.feishuBotAppId) process.stderr.write(` Feishu: bot bound (${mergedArgs.feishuBotAppId})\n`);
2395
- setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", serverUrl);
2396
- if (mergedArgs.type === "human") {
2181
+ if (args.feishuBotAppId) process.stderr.write(` Feishu: bot bound (${args.feishuBotAppId})\n`);
2182
+ if (args.type === "human") {
2397
2183
  process.stderr.write("\n Next step — bind your Feishu account:\n");
2398
- process.stderr.write(` Send this message to the bot in Feishu: /bind ${mergedArgs.id}\n`);
2399
- 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");
2400
2186
  }
2401
2187
  process.stderr.write("\n Start the agent:\n");
2402
2188
  process.stderr.write(" first-tree-hub client start\n");
2403
2189
  process.stderr.write("\n");
2404
2190
  }
2405
- function createMemberNodeMd(repoPath, data) {
2406
- const memberDir = join(repoPath, data.parentPath ?? "members", data.id);
2407
- mkdirSync(memberDir, { recursive: true });
2408
- const domainsList = data.domains.map((d) => ` - "${d}"`).join("\n");
2409
- const githubLine = data.github ? `\ngithub: ${data.github}` : "";
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
- `;
2420
- const content = `---
2421
- title: "${data.displayName}"
2422
- owners: [${data.owner}]
2423
- type: ${data.type}
2424
- role: "${data.role}"
2425
- domains:
2426
- ${domainsList}${githubLine}${delegateLine}
2427
- ---
2428
-
2429
- # ${data.displayName}
2430
-
2431
- ${bodySections}`;
2432
- writeFileSync(join(memberDir, "NODE.md"), content);
2433
- }
2434
- function isTrackedByGit(repoPath, filePath) {
2435
- try {
2436
- execSync(`git ls-files --error-unmatch ${filePath}`, {
2437
- cwd: repoPath,
2438
- stdio: "pipe"
2439
- });
2440
- return true;
2441
- } catch {
2442
- return false;
2443
- }
2444
- }
2445
- const CONTEXT_TREE_DIR = join(DEFAULT_HOME_DIR$1, "context-tree");
2446
- /**
2447
- * Resolve Context Tree to a **local path** at $FIRST_TREE_HUB_HOME/context-tree/.
2448
- *
2449
- * Repo URL is obtained from the Hub server. The local clone is always
2450
- * managed in the standard location — no custom paths allowed.
2451
- */
2452
- async function resolveContextTreeRepo(serverUrl) {
2453
- const repoUrl = await fetchRepoUrlFromServer(serverUrl);
2454
- if (!repoUrl) return null;
2455
- let ghToken;
2456
- try {
2457
- ghToken = execSync("gh auth token", {
2458
- encoding: "utf-8",
2459
- stdio: "pipe"
2460
- }).trim();
2461
- } catch {
2462
- return null;
2463
- }
2464
- const gitEnv = {
2465
- ...process.env,
2466
- GIT_ASKPASS: "echo",
2467
- GIT_TERMINAL_PROMPT: "0",
2468
- GH_TOKEN: ghToken,
2469
- GITHUB_TOKEN: ghToken
2470
- };
2471
- const gitConfigArgs = `-c url."https://x-access-token:${ghToken}@github.com/".insteadOf="https://github.com/"`;
2472
- if (existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
2473
- try {
2474
- if (execSync("git remote get-url origin", {
2475
- cwd: CONTEXT_TREE_DIR,
2476
- encoding: "utf-8",
2477
- stdio: "pipe"
2478
- }).trim().includes(repoUrl.replace(/^https?:\/\/github\.com\//, "").replace(/\.git$/, ""))) {
2479
- process.stderr.write("Updating Context Tree...\n");
2480
- execSync("git checkout main 2>/dev/null || git checkout master", {
2481
- cwd: CONTEXT_TREE_DIR,
2482
- stdio: "pipe"
2483
- });
2484
- try {
2485
- execSync(`git ${gitConfigArgs} pull --ff-only`, {
2486
- cwd: CONTEXT_TREE_DIR,
2487
- stdio: "pipe",
2488
- env: gitEnv
2489
- });
2490
- } catch {}
2491
- return CONTEXT_TREE_DIR;
2492
- }
2493
- } catch {}
2494
- const safePrefix = DEFAULT_HOME_DIR$1;
2495
- if (!CONTEXT_TREE_DIR.startsWith(safePrefix) || CONTEXT_TREE_DIR === safePrefix) throw new Error(`Refusing to delete unsafe path: ${CONTEXT_TREE_DIR}`);
2496
- execSync(`rm -rf ${CONTEXT_TREE_DIR}`);
2497
- }
2498
- try {
2499
- process.stderr.write(`Cloning Context Tree to ${CONTEXT_TREE_DIR}...\n`);
2500
- mkdirSync(DEFAULT_HOME_DIR$1, { recursive: true });
2501
- execSync(`git ${gitConfigArgs} clone ${repoUrl} ${CONTEXT_TREE_DIR}`, {
2502
- stdio: "pipe",
2503
- env: gitEnv
2504
- });
2505
- return CONTEXT_TREE_DIR;
2506
- } catch {
2507
- return null;
2508
- }
2509
- }
2510
- /** Query server for Context Tree repo URL. */
2511
- async function fetchRepoUrlFromServer(serverUrl) {
2512
- if (!serverUrl) try {
2513
- serverUrl = resolveServerUrl();
2514
- } catch {
2515
- return null;
2516
- }
2517
- try {
2518
- const url = `${serverUrl.replace(/\/+$/, "")}/api/v1/context-tree/info`;
2519
- const res = await fetch(url, { signal: AbortSignal.timeout(5e3) });
2520
- if (!res.ok) return null;
2521
- return (await res.json()).repo ?? null;
2522
- } catch {
2523
- return null;
2524
- }
2525
- }
2526
- function sleep(ms) {
2527
- return new Promise((resolve) => setTimeout(resolve, ms));
2528
- }
2529
- function stripUndefined(obj) {
2530
- const result = {};
2531
- for (const [key, value] of Object.entries(obj)) if (value !== void 0) result[key] = value;
2532
- return result;
2533
- }
2534
2191
  //#endregion
2535
2192
  //#region src/core/prompt.ts
2536
2193
  /**
@@ -2750,38 +2407,30 @@ const AGENT_STATUSES = {
2750
2407
  DELETED: "deleted"
2751
2408
  };
2752
2409
  z.enum(["active", "suspended"]);
2753
- z.object({
2754
- id: z.string().min(1).max(100).regex(/^[a-z0-9_-]+$/, "Only lowercase alphanumeric, hyphens, and underscores").optional(),
2410
+ const createAgentSchema = z.object({
2411
+ name: z.string().min(1).max(100).regex(/^[a-z0-9_-]+$/, "Only lowercase alphanumeric, hyphens, and underscores").optional(),
2755
2412
  type: agentTypeSchema,
2756
2413
  displayName: z.string().max(200).optional(),
2414
+ delegateMention: z.string().max(100).optional(),
2415
+ profile: z.string().optional(),
2757
2416
  organizationId: z.string().max(100).optional(),
2758
2417
  metadata: z.record(z.string(), z.unknown()).optional()
2759
2418
  });
2760
- z.object({
2761
- syncedAt: z.string(),
2762
- treePath: z.string(),
2763
- summary: z.object({
2764
- created: z.number(),
2765
- updated: z.number(),
2766
- suspended: z.number(),
2767
- unchanged: z.number(),
2768
- errors: z.number()
2769
- }),
2770
- created: z.array(z.string()),
2771
- updated: z.array(z.string()),
2772
- suspended: z.array(z.string()),
2773
- errors: z.array(z.object({
2774
- memberId: z.string(),
2775
- error: z.string()
2776
- }))
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()
2777
2425
  });
2778
2426
  z.object({
2779
- id: z.string(),
2427
+ uuid: z.string(),
2428
+ name: z.string().nullable(),
2780
2429
  organizationId: z.string(),
2781
2430
  type: agentTypeSchema,
2782
2431
  displayName: z.string().nullable(),
2783
2432
  delegateMention: z.string().nullable(),
2784
- treePath: z.string().nullable(),
2433
+ profile: z.string().nullable(),
2785
2434
  inboxId: z.string(),
2786
2435
  status: z.string(),
2787
2436
  metadata: z.record(z.string(), z.unknown()),
@@ -2789,15 +2438,21 @@ z.object({
2789
2438
  createdAt: z.string(),
2790
2439
  updatedAt: z.string()
2791
2440
  });
2792
- const bootstrapTokenRequestSchema = z.object({ name: z.string().max(100).optional() });
2441
+ const bootstrapTokenRequestSchema = z.object({
2442
+ name: z.string().max(100).optional(),
2443
+ type: agentTypeSchema.optional(),
2444
+ displayName: z.string().max(200).optional(),
2445
+ delegateMention: z.string().max(100).optional(),
2446
+ profile: z.string().optional(),
2447
+ metadata: z.record(z.string(), z.unknown()).optional()
2448
+ });
2793
2449
  z.object({
2794
2450
  exists: z.boolean(),
2795
2451
  status: z.enum(["active", "suspended"]).nullable()
2796
2452
  });
2797
2453
  z.object({
2798
- repo: z.string(),
2799
- branch: z.string(),
2800
- lastSync: z.string().nullable()
2454
+ repo: z.string().nullable(),
2455
+ branch: z.string().nullable()
2801
2456
  });
2802
2457
  const createAgentTokenSchema = z.object({
2803
2458
  name: z.string().max(100).optional(),
@@ -2919,7 +2574,7 @@ const SYSTEM_CONFIG_DEFAULTS = {
2919
2574
  [SYSTEM_CONFIG_KEYS.PRESENCE_CLEANUP_SECONDS]: 60
2920
2575
  };
2921
2576
  //#endregion
2922
- //#region ../server/dist/app-CurdzcN2.mjs
2577
+ //#region ../server/dist/app-dUnTcJpC.mjs
2923
2578
  var __defProp = Object.defineProperty;
2924
2579
  var __exportAll = (all, no_symbols) => {
2925
2580
  let target = {};
@@ -2932,24 +2587,25 @@ var __exportAll = (all, no_symbols) => {
2932
2587
  };
2933
2588
  /** Agent registration. Each agent owns a unique inboxId for message delivery. */
2934
2589
  const agents = pgTable("agents", {
2935
- id: text("id").primaryKey(),
2590
+ uuid: text("uuid").primaryKey(),
2591
+ name: text("name"),
2936
2592
  organizationId: text("organization_id").notNull().default("default"),
2937
2593
  type: text("type").notNull(),
2938
2594
  displayName: text("display_name"),
2939
2595
  delegateMention: text("delegate_mention"),
2940
- treePath: text("tree_path"),
2596
+ profile: text("profile"),
2941
2597
  inboxId: text("inbox_id").unique().notNull(),
2942
2598
  status: text("status").notNull().default("active"),
2943
2599
  metadata: jsonb("metadata").$type().notNull().default({}),
2944
2600
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2945
2601
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
2946
- }, (table) => [index("idx_agents_org").on(table.organizationId)]);
2602
+ }, (table) => [index("idx_agents_org").on(table.organizationId), unique("uq_agents_org_name").on(table.organizationId, table.name)]);
2947
2603
  /** Maps external user identities to internal Agents. */
2948
2604
  const adapterAgentMappings = pgTable("adapter_agent_mappings", {
2949
2605
  id: serial("id").primaryKey(),
2950
2606
  platform: text("platform").notNull(),
2951
2607
  externalUserId: text("external_user_id").notNull(),
2952
- agentId: text("agent_id").notNull().references(() => agents.id),
2608
+ agentId: text("agent_id").notNull().references(() => agents.uuid),
2953
2609
  boundVia: text("bound_via"),
2954
2610
  displayName: text("display_name"),
2955
2611
  metadata: jsonb("metadata").$type().notNull().default({}),
@@ -3007,7 +2663,7 @@ const chats = pgTable("chats", {
3007
2663
  /** Chat participants (M:N). */
3008
2664
  const chatParticipants = pgTable("chat_participants", {
3009
2665
  chatId: text("chat_id").notNull().references(() => chats.id, { onDelete: "cascade" }),
3010
- agentId: text("agent_id").notNull().references(() => agents.id),
2666
+ agentId: text("agent_id").notNull().references(() => agents.uuid),
3011
2667
  role: text("role").notNull().default("member"),
3012
2668
  mode: text("mode").notNull().default("full"),
3013
2669
  joinedAt: timestamp("joined_at", { withTimezone: true }).notNull().defaultNow()
@@ -3124,7 +2780,7 @@ async function findOrCreateChatForChannel(db, data) {
3124
2780
  const chatId = randomUUID();
3125
2781
  const internalType = data.chatType === "p2p" ? "direct" : "group";
3126
2782
  return db.transaction(async (tx) => {
3127
- const [botAgent] = await tx.select({ organizationId: agents.organizationId }).from(agents).where(eq(agents.id, data.botAgentId)).limit(1);
2783
+ const [botAgent] = await tx.select({ organizationId: agents.organizationId }).from(agents).where(eq(agents.uuid, data.botAgentId)).limit(1);
3128
2784
  const orgId = botAgent?.organizationId ?? "default";
3129
2785
  await tx.insert(chats).values({
3130
2786
  id: chatId,
@@ -3207,10 +2863,10 @@ async function adminAdapterMappingRoutes(app) {
3207
2863
  app.post("/", async (request, reply) => {
3208
2864
  const body = createAdapterMappingSchema.parse(request.body);
3209
2865
  const [agent] = await app.db.select({
3210
- id: agents.id,
2866
+ id: agents.uuid,
3211
2867
  type: agents.type,
3212
2868
  status: agents.status
3213
- }).from(agents).where(eq(agents.id, body.agentId)).limit(1);
2869
+ }).from(agents).where(eq(agents.uuid, body.agentId)).limit(1);
3214
2870
  if (!agent || agent.status === "deleted") throw new NotFoundError(`Agent "${body.agentId}" not found`);
3215
2871
  if (agent.type !== "human") throw new BadRequestError("User bindings can only be created for human agents");
3216
2872
  const row = await createAgentMapping(app.db, {
@@ -3246,7 +2902,7 @@ async function adminAdapterStatusRoutes(app) {
3246
2902
  const adapterConfigs = pgTable("adapter_configs", {
3247
2903
  id: serial("id").primaryKey(),
3248
2904
  platform: text("platform").notNull(),
3249
- agentId: text("agent_id").notNull().references(() => agents.id),
2905
+ agentId: text("agent_id").notNull().references(() => agents.uuid),
3250
2906
  credentials: jsonb("credentials").$type().notNull(),
3251
2907
  status: text("status").notNull().default("active"),
3252
2908
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
@@ -3308,9 +2964,9 @@ function requireEncryptionKey(key) {
3308
2964
  }
3309
2965
  async function validateAgentId(db, agentId) {
3310
2966
  const [agent] = await db.select({
3311
- id: agents.id,
2967
+ id: agents.uuid,
3312
2968
  type: agents.type
3313
- }).from(agents).where(and(eq(agents.id, agentId), ne(agents.status, "deleted"))).limit(1);
2969
+ }).from(agents).where(and(eq(agents.uuid, agentId), ne(agents.status, "deleted"))).limit(1);
3314
2970
  if (!agent) throw new NotFoundError(`Agent "${agentId}" not found`);
3315
2971
  if (agent.type === "human") throw new BadRequestError("Adapter configs can only be bound to non-human agents");
3316
2972
  }
@@ -3423,292 +3079,9 @@ async function adminAdapterRoutes(app) {
3423
3079
  return reply.status(204).send();
3424
3080
  });
3425
3081
  }
3426
- const GRAPHQL_URL = "https://api.github.com/graphql";
3427
- const REST_API_URL = "https://api.github.com";
3428
- /** Parse "owner/repo" or "https://github.com/owner/repo" into { owner, name }. */
3429
- function parseRepo(input) {
3430
- const urlMatch = /github\.com\/([^/]+)\/([^/.]+)/.exec(input);
3431
- if (urlMatch) return {
3432
- owner: urlMatch[1] ?? "",
3433
- name: urlMatch[2] ?? ""
3434
- };
3435
- const parts = input.split("/");
3436
- return {
3437
- owner: parts[0] ?? "",
3438
- name: parts[1] ?? ""
3439
- };
3440
- }
3441
- /** Step 1: Get the tree OID of the members/ directory via GraphQL. */
3442
- async function fetchMembersTreeOid(owner, name, branch, token) {
3443
- const query = `
3444
- query($owner: String!, $name: String!, $expr: String!) {
3445
- repository(owner: $owner, name: $name) {
3446
- object(expression: $expr) {
3447
- ... on Tree { oid }
3448
- }
3449
- }
3450
- }
3451
- `;
3452
- const res = await fetch(GRAPHQL_URL, {
3453
- method: "POST",
3454
- headers: {
3455
- Authorization: `Bearer ${token}`,
3456
- "Content-Type": "application/json"
3457
- },
3458
- body: JSON.stringify({
3459
- query,
3460
- variables: {
3461
- owner,
3462
- name,
3463
- expr: `${branch}:members`
3464
- }
3465
- })
3466
- });
3467
- if (!res.ok) throw new Error(`GitHub API returned ${res.status}: ${await res.text()}`);
3468
- const json = await res.json();
3469
- if (json.errors) throw new Error(`GitHub GraphQL errors: ${json.errors.map((e) => e.message).join(", ")}`);
3470
- return json.data?.repository?.object?.oid ?? null;
3471
- }
3472
- /** Step 2: Recursively list all entries under the members/ tree via REST API. */
3473
- async function fetchRecursiveTree(owner, name, treeSha, token) {
3474
- const url = `${REST_API_URL}/repos/${owner}/${name}/git/trees/${treeSha}?recursive=1`;
3475
- const res = await fetch(url, { headers: {
3476
- Authorization: `Bearer ${token}`,
3477
- Accept: "application/vnd.github+json"
3478
- } });
3479
- if (!res.ok) throw new Error(`GitHub REST API returned ${res.status}: ${await res.text()}`);
3480
- const json = await res.json();
3481
- 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.");
3482
- return json.tree;
3483
- }
3484
- /**
3485
- * Step 3: Batch-fetch NODE.md content for all member directories via GraphQL aliases.
3486
- * Each alias fetches one NODE.md file by expression.
3487
- */
3488
- async function batchFetchNodeMd(owner, name, branch, memberPaths, token) {
3489
- if (memberPaths.length === 0) return /* @__PURE__ */ new Map();
3490
- const query = `
3491
- query($owner: String!, $name: String!) {
3492
- repository(owner: $owner, name: $name) {
3493
- ${memberPaths.map((p, i) => {
3494
- const expr = `${branch}:members/${p}/NODE.md`;
3495
- return `m${i}: object(expression: ${JSON.stringify(expr)}) { ... on Blob { text } }`;
3496
- }).join("\n ")}
3497
- }
3498
- }
3499
- `;
3500
- const res = await fetch(GRAPHQL_URL, {
3501
- method: "POST",
3502
- headers: {
3503
- Authorization: `Bearer ${token}`,
3504
- "Content-Type": "application/json"
3505
- },
3506
- body: JSON.stringify({
3507
- query,
3508
- variables: {
3509
- owner,
3510
- name
3511
- }
3512
- })
3513
- });
3514
- if (!res.ok) throw new Error(`GitHub API returned ${res.status}: ${await res.text()}`);
3515
- const json = await res.json();
3516
- if (json.errors) throw new Error(`GitHub GraphQL errors: ${json.errors.map((e) => e.message).join(", ")}`);
3517
- const repo = json.data?.repository ?? {};
3518
- const result = /* @__PURE__ */ new Map();
3519
- for (let i = 0; i < memberPaths.length; i++) {
3520
- const blob = repo[`m${i}`];
3521
- const path = memberPaths[i];
3522
- if (blob?.text && path) result.set(path, blob.text);
3523
- }
3524
- return result;
3525
- }
3526
- /**
3527
- * Extract member directory paths from a recursive tree listing.
3528
- * A directory is a member if it contains a NODE.md blob.
3529
- */
3530
- function extractMemberDirs(treeEntries) {
3531
- const nodeMdPaths = /* @__PURE__ */ new Set();
3532
- for (const entry of treeEntries) if (entry.type === "blob" && entry.path.endsWith("/NODE.md")) nodeMdPaths.add(entry.path);
3533
- const memberDirs = [];
3534
- for (const entry of treeEntries) {
3535
- if (entry.type !== "tree") continue;
3536
- if (nodeMdPaths.has(`${entry.path}/NODE.md`)) memberDirs.push(entry.path);
3537
- }
3538
- return memberDirs.sort();
3539
- }
3540
- /**
3541
- * Fetch all members from a Context Tree repo via GitHub API.
3542
- * Uses 3 API calls:
3543
- * 1. GraphQL: get members/ tree OID
3544
- * 2. REST: recursive tree listing (scoped to members/ only)
3545
- * 3. GraphQL: batch-fetch all NODE.md contents via aliases
3546
- */
3547
- async function fetchMembers(repo, branch, token) {
3548
- const { owner, name } = parseRepo(repo);
3549
- if (!owner || !name) throw new Error(`Invalid repo format: "${repo}" — expected "owner/repo" or a GitHub URL`);
3550
- const treeOid = await fetchMembersTreeOid(owner, name, branch, token);
3551
- if (!treeOid) {
3552
- console.warn("[context-tree-sync] members/ directory not found in repo");
3553
- return [];
3554
- }
3555
- const memberDirs = extractMemberDirs(await fetchRecursiveTree(owner, name, treeOid, token));
3556
- if (memberDirs.length === 0) {
3557
- console.warn("[context-tree-sync] No member directories with NODE.md found");
3558
- return [];
3559
- }
3560
- const nameMap = /* @__PURE__ */ new Map();
3561
- for (const dir of memberDirs) {
3562
- const dirName = dir.split("/").pop() ?? dir;
3563
- const existing = nameMap.get(dirName);
3564
- 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.`);
3565
- nameMap.set(dirName, dir);
3566
- }
3567
- const nodeContents = await batchFetchNodeMd(owner, name, branch, memberDirs, token);
3568
- return memberDirs.map((dir) => ({
3569
- name: dir.split("/").pop() ?? dir,
3570
- treePath: dir,
3571
- nodeContent: nodeContents.get(dir) ?? null
3572
- }));
3573
- }
3574
- /** Parse NODE.md frontmatter for agent metadata. */
3575
- function parseNodeMetadata(content) {
3576
- const match = /^---\n([\s\S]*?)\n---/.exec(content);
3577
- if (!match) return {
3578
- type: "autonomous_agent",
3579
- displayName: null,
3580
- delegateMention: null,
3581
- owners: [],
3582
- github: null
3583
- };
3584
- const frontmatter = match[1] ?? "";
3585
- const getValue = (key) => {
3586
- const lineMatch = new RegExp(`^${key}:\\s*(.+)$`, "m").exec(frontmatter);
3587
- return lineMatch ? lineMatch[1]?.trim().replace(/^["']|["']$/g, "") ?? null : null;
3588
- };
3589
- const ownersRaw = getValue("owners");
3590
- let owners = [];
3591
- if (ownersRaw) {
3592
- const listMatch = /^\[([^\]]*)\]$/.exec(ownersRaw);
3593
- if (listMatch?.[1]) owners = listMatch[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
3594
- }
3595
- return {
3596
- type: getValue("type") ?? "autonomous_agent",
3597
- displayName: getValue("display_name") ?? getValue("title") ?? getValue("name"),
3598
- delegateMention: getValue("delegate_mention"),
3599
- owners,
3600
- github: getValue("github")
3601
- };
3602
- }
3603
- /** Stored for the /status endpoint */
3604
- let _lastSyncResult;
3605
- function getLastGraphQLSyncResult() {
3606
- return _lastSyncResult;
3607
- }
3608
- /**
3609
- * Sync agents from a GitHub Context Tree repo via GraphQL.
3610
- *
3611
- * Lifecycle semantics:
3612
- * - Member in tree, not in DB → create (active)
3613
- * - Member in tree, in DB as active, fields changed → update
3614
- * - Member in tree, in DB as active, fields unchanged → unchanged
3615
- * - Member in tree, in DB as suspended → reactivate (set active)
3616
- * - Agent in DB as active, NOT in tree → suspend
3617
- */
3618
- async function syncFromGitHub(db, repo, branch, githubToken) {
3619
- const members = await fetchMembers(repo, branch, githubToken);
3620
- const memberNames = new Set(members.map((m) => m.name));
3621
- const result = {
3622
- created: 0,
3623
- updated: 0,
3624
- suspended: 0,
3625
- reactivated: 0,
3626
- unchanged: 0,
3627
- errors: 0,
3628
- syncedAt: (/* @__PURE__ */ new Date()).toISOString()
3629
- };
3630
- for (const member of members) try {
3631
- const meta = member.nodeContent ? parseNodeMetadata(member.nodeContent) : {
3632
- type: "autonomous_agent",
3633
- displayName: null,
3634
- delegateMention: null,
3635
- owners: [],
3636
- github: null
3637
- };
3638
- const metadataJson = JSON.stringify({
3639
- owners: meta.owners,
3640
- github: meta.github
3641
- });
3642
- const existing = await db.execute(sql`SELECT id, status, type, display_name, delegate_mention, tree_path, metadata FROM agents WHERE id = ${member.name}`);
3643
- if (existing.length === 0) {
3644
- await db.execute(sql`
3645
- INSERT INTO agents (id, type, display_name, delegate_mention, tree_path, status, inbox_id, metadata)
3646
- VALUES (${member.name}, ${meta.type}, ${meta.displayName}, ${meta.delegateMention}, ${member.treePath}, 'active', ${`inbox_${member.name}`}, ${metadataJson}::jsonb)
3647
- `);
3648
- result.created++;
3649
- } else {
3650
- const agent = existing[0];
3651
- const existingMeta = agent.metadata ?? {};
3652
- const mergedMeta = JSON.stringify({
3653
- ...existingMeta,
3654
- owners: meta.owners,
3655
- github: meta.github
3656
- });
3657
- if (agent.status === "suspended" || agent.status === "deleted") {
3658
- await db.execute(sql`
3659
- UPDATE agents SET status = 'active', type = ${meta.type}, display_name = ${meta.displayName}, delegate_mention = ${meta.delegateMention}, tree_path = ${member.treePath}, metadata = ${mergedMeta}::jsonb
3660
- WHERE id = ${member.name}
3661
- `);
3662
- result.reactivated++;
3663
- } 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)) {
3664
- await db.execute(sql`
3665
- UPDATE agents SET type = ${meta.type}, display_name = ${meta.displayName}, delegate_mention = ${meta.delegateMention}, tree_path = ${member.treePath}, metadata = ${mergedMeta}::jsonb
3666
- WHERE id = ${member.name}
3667
- `);
3668
- result.updated++;
3669
- } else result.unchanged++;
3670
- }
3671
- } catch (err) {
3672
- console.error(`[context-tree-sync] Failed to sync member "${member.name}" (path: members/${member.treePath}):`, err);
3673
- result.errors++;
3674
- }
3675
- try {
3676
- const activeAgents = await db.execute(sql`SELECT id FROM agents WHERE status = 'active' AND (metadata->>'managed')::boolean IS NOT TRUE`);
3677
- for (const row of activeAgents) {
3678
- const agent = row;
3679
- if (!memberNames.has(agent.id)) {
3680
- await db.execute(sql`UPDATE agents SET status = 'suspended' WHERE id = ${agent.id}`);
3681
- result.suspended++;
3682
- }
3683
- }
3684
- } catch (err) {
3685
- console.error("[context-tree-sync] Failed to suspend removed agents:", err);
3686
- result.errors++;
3687
- }
3688
- _lastSyncResult = result;
3689
- return result;
3690
- }
3691
- async function adminAgentSyncRoutes(app) {
3692
- app.post("/", async (_request, reply) => {
3693
- const { repo, branch } = app.config.contextTree;
3694
- const { token } = app.config.github;
3695
- try {
3696
- const result = await syncFromGitHub(app.db, repo, branch, token);
3697
- return reply.send({ summary: result });
3698
- } catch (error) {
3699
- const msg = error instanceof Error ? error.message : String(error);
3700
- app.log.error(error, "Context Tree sync failed");
3701
- return reply.status(502).send({ error: msg });
3702
- }
3703
- });
3704
- app.get("/status", async (_request, reply) => {
3705
- const lastSync = getLastGraphQLSyncResult();
3706
- return reply.send({ lastSync: lastSync ?? null });
3707
- });
3708
- }
3709
3082
  /** Agent online status. Tracked via WebSocket connections; stale entries are cleaned up using server_instances heartbeat. */
3710
3083
  const agentPresence = pgTable("agent_presence", {
3711
- agentId: text("agent_id").primaryKey().references(() => agents.id, { onDelete: "cascade" }),
3084
+ agentId: text("agent_id").primaryKey().references(() => agents.uuid, { onDelete: "cascade" }),
3712
3085
  status: text("status").notNull().default("offline"),
3713
3086
  instanceId: text("instance_id"),
3714
3087
  connectedAt: timestamp("connected_at", { withTimezone: true }),
@@ -3717,7 +3090,7 @@ const agentPresence = pgTable("agent_presence", {
3717
3090
  /** Agent bearer tokens. Multiple tokens can coexist for zero-downtime rotation. */
3718
3091
  const agentTokens = pgTable("agent_tokens", {
3719
3092
  id: text("id").primaryKey(),
3720
- agentId: text("agent_id").notNull().references(() => agents.id, { onDelete: "cascade" }),
3093
+ agentId: text("agent_id").notNull().references(() => agents.uuid, { onDelete: "cascade" }),
3721
3094
  tokenHash: text("token_hash").notNull(),
3722
3095
  name: text("name"),
3723
3096
  expiresAt: timestamp("expires_at", { withTimezone: true }),
@@ -3725,62 +3098,83 @@ const agentTokens = pgTable("agent_tokens", {
3725
3098
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
3726
3099
  lastUsedAt: timestamp("last_used_at", { withTimezone: true })
3727
3100
  }, (table) => [index("idx_agent_tokens_agent").on(table.agentId), index("idx_agent_tokens_hash").on(table.tokenHash)]);
3101
+ /** Generate a UUID v7 (time-ordered). No external dependency. */
3102
+ function uuidv7() {
3103
+ const now = BigInt(Date.now());
3104
+ const bytes = new Uint8Array(16);
3105
+ bytes[0] = Number(now >> 40n & 255n);
3106
+ bytes[1] = Number(now >> 32n & 255n);
3107
+ bytes[2] = Number(now >> 24n & 255n);
3108
+ bytes[3] = Number(now >> 16n & 255n);
3109
+ bytes[4] = Number(now >> 8n & 255n);
3110
+ bytes[5] = Number(now & 255n);
3111
+ const rand = randomBytes(10);
3112
+ for (let i = 0; i < 10; i++) {
3113
+ const b = rand[i];
3114
+ if (b !== void 0) bytes[6 + i] = b;
3115
+ }
3116
+ bytes[6] = (bytes[6] ?? 0) & 15 | 112;
3117
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
3118
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
3119
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
3120
+ }
3728
3121
  function hashToken$1(raw) {
3729
3122
  return createHash("sha256").update(raw).digest("hex");
3730
3123
  }
3731
3124
  async function createAgent(db, data) {
3732
- const id = data.id ?? randomUUID();
3733
- const inboxId = `inbox_${id}`;
3734
- const [existing] = await db.select({
3735
- id: agents.id,
3736
- status: agents.status
3737
- }).from(agents).where(eq(agents.id, id)).limit(1);
3738
- if (existing) {
3739
- if (existing.status !== AGENT_STATUSES.DELETED) throw new ConflictError(`Agent "${id}" already exists`);
3740
- const [agent] = await db.update(agents).set({
3741
- organizationId: data.organizationId ?? "default",
3125
+ const uuid = uuidv7();
3126
+ const name = data.name ?? null;
3127
+ const inboxId = `inbox_${uuid}`;
3128
+ const orgId = data.organizationId ?? "default";
3129
+ try {
3130
+ const [agent] = await db.insert(agents).values({
3131
+ uuid,
3132
+ name,
3133
+ organizationId: orgId,
3742
3134
  type: data.type,
3743
3135
  displayName: data.displayName ?? null,
3744
- status: "active",
3745
- metadata: data.metadata ?? {},
3746
- updatedAt: /* @__PURE__ */ new Date()
3747
- }).where(eq(agents.id, id)).returning();
3748
- if (!agent) throw new Error("Unexpected: UPDATE RETURNING produced no row");
3136
+ delegateMention: data.delegateMention ?? null,
3137
+ profile: data.profile ?? null,
3138
+ inboxId,
3139
+ metadata: data.metadata ?? {}
3140
+ }).returning();
3141
+ if (!agent) throw new Error("Unexpected: INSERT RETURNING produced no row");
3749
3142
  return agent;
3143
+ } catch (err) {
3144
+ if ((err?.code ?? err?.cause?.code ?? "") === "23505" && name) throw new ConflictError(`Agent name "${name}" already exists in organization "${orgId}"`);
3145
+ throw err;
3750
3146
  }
3751
- const [agent] = await db.insert(agents).values({
3752
- id,
3753
- organizationId: data.organizationId ?? "default",
3754
- type: data.type,
3755
- displayName: data.displayName ?? null,
3756
- inboxId,
3757
- metadata: data.metadata ?? {}
3758
- }).returning();
3759
- if (!agent) throw new Error("Unexpected: INSERT RETURNING produced no row");
3147
+ }
3148
+ async function getAgent(db, uuid) {
3149
+ const [agent] = await db.select().from(agents).where(and(eq(agents.uuid, uuid), ne(agents.status, AGENT_STATUSES.DELETED))).limit(1);
3150
+ if (!agent) throw new NotFoundError(`Agent "${uuid}" not found`);
3760
3151
  return agent;
3761
3152
  }
3762
- async function getAgent(db, id) {
3763
- const [agent] = await db.select().from(agents).where(and(eq(agents.id, id), ne(agents.status, AGENT_STATUSES.DELETED))).limit(1);
3764
- if (!agent) throw new NotFoundError(`Agent "${id}" not found`);
3153
+ async function getAgentByName(db, orgId, name) {
3154
+ const [agent] = await db.select().from(agents).where(and(eq(agents.organizationId, orgId), eq(agents.name, name), ne(agents.status, AGENT_STATUSES.DELETED))).limit(1);
3155
+ if (!agent) throw new NotFoundError(`Agent "${name}" not found in organization "${orgId}"`);
3765
3156
  return agent;
3766
3157
  }
3767
- async function listAgents(db, limit, cursor) {
3768
- const notDeleted = ne(agents.status, AGENT_STATUSES.DELETED);
3769
- const where = cursor ? and(notDeleted, lt(agents.createdAt, new Date(cursor))) : notDeleted;
3158
+ async function listAgents(db, orgId, limit, cursor, type) {
3159
+ const conditions = [ne(agents.status, AGENT_STATUSES.DELETED), eq(agents.organizationId, orgId)];
3160
+ if (cursor) conditions.push(lt(agents.createdAt, new Date(cursor)));
3161
+ if (type) conditions.push(eq(agents.type, type));
3162
+ const where = and(...conditions);
3770
3163
  const rows = await db.select({
3771
- id: agents.id,
3164
+ uuid: agents.uuid,
3165
+ name: agents.name,
3772
3166
  organizationId: agents.organizationId,
3773
3167
  type: agents.type,
3774
3168
  displayName: agents.displayName,
3775
3169
  delegateMention: agents.delegateMention,
3776
- treePath: agents.treePath,
3170
+ profile: agents.profile,
3777
3171
  inboxId: agents.inboxId,
3778
3172
  status: agents.status,
3779
3173
  metadata: agents.metadata,
3780
3174
  createdAt: agents.createdAt,
3781
3175
  updatedAt: agents.updatedAt,
3782
3176
  presenceStatus: agentPresence.status
3783
- }).from(agents).leftJoin(agentPresence, eq(agents.id, agentPresence.agentId)).where(where).orderBy(desc(agents.createdAt)).limit(limit + 1);
3177
+ }).from(agents).leftJoin(agentPresence, eq(agents.uuid, agentPresence.agentId)).where(where).orderBy(desc(agents.createdAt)).limit(limit + 1);
3784
3178
  const hasMore = rows.length > limit;
3785
3179
  const items = hasMore ? rows.slice(0, limit) : rows;
3786
3180
  const last = items[items.length - 1];
@@ -3789,45 +3183,115 @@ async function listAgents(db, limit, cursor) {
3789
3183
  nextCursor: hasMore && last ? last.createdAt.toISOString() : null
3790
3184
  };
3791
3185
  }
3186
+ async function updateAgent(db, uuid, data) {
3187
+ const agent = await getAgent(db, uuid);
3188
+ const updates = { updatedAt: /* @__PURE__ */ new Date() };
3189
+ if (data.type !== void 0) updates.type = data.type;
3190
+ if (data.displayName !== void 0) updates.displayName = data.displayName;
3191
+ if (data.delegateMention !== void 0) updates.delegateMention = data.delegateMention;
3192
+ if (data.profile !== void 0) updates.profile = data.profile;
3193
+ if (data.metadata !== void 0) updates.metadata = data.metadata;
3194
+ const [updated] = await db.update(agents).set(updates).where(eq(agents.uuid, agent.uuid)).returning();
3195
+ if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
3196
+ return updated;
3197
+ }
3198
+ /**
3199
+ * Reactivate a suspended agent.
3200
+ */
3201
+ async function reactivateAgent(db, uuid) {
3202
+ const [existing] = await db.select({
3203
+ uuid: agents.uuid,
3204
+ status: agents.status
3205
+ }).from(agents).where(eq(agents.uuid, uuid)).limit(1);
3206
+ if (!existing || existing.status === AGENT_STATUSES.DELETED) throw new NotFoundError(`Agent "${uuid}" not found`);
3207
+ if (existing.status !== AGENT_STATUSES.SUSPENDED) throw new BadRequestError("Only suspended agents can be reactivated.");
3208
+ const [agent] = await db.update(agents).set({
3209
+ status: AGENT_STATUSES.ACTIVE,
3210
+ updatedAt: /* @__PURE__ */ new Date()
3211
+ }).where(eq(agents.uuid, uuid)).returning();
3212
+ if (!agent) throw new Error("Unexpected: UPDATE RETURNING produced no row");
3213
+ return agent;
3214
+ }
3215
+ /**
3216
+ * Suspend an agent. Revokes all active tokens so the agent can no longer authenticate.
3217
+ */
3218
+ async function suspendAgent(db, uuid) {
3219
+ const [agent] = await db.update(agents).set({
3220
+ status: AGENT_STATUSES.SUSPENDED,
3221
+ updatedAt: /* @__PURE__ */ new Date()
3222
+ }).where(and(eq(agents.uuid, uuid), ne(agents.status, AGENT_STATUSES.DELETED))).returning();
3223
+ if (!agent) throw new NotFoundError(`Agent "${uuid}" not found`);
3224
+ await db.update(agentTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and(eq(agentTokens.agentId, uuid), isNull(agentTokens.revokedAt)));
3225
+ return agent;
3226
+ }
3792
3227
  /**
3793
- * Delete an agent. Only allowed when status is "suspended" (removed from tree).
3228
+ * Delete an agent. Only allowed when status is "suspended".
3229
+ * Suspend the agent first to revoke tokens, then delete.
3230
+ * Sets name to NULL to release the name for reuse.
3794
3231
  */
3795
- async function deleteAgent(db, id) {
3232
+ async function deleteAgent(db, uuid) {
3796
3233
  const [existing] = await db.select({
3797
- id: agents.id,
3234
+ uuid: agents.uuid,
3798
3235
  status: agents.status
3799
- }).from(agents).where(eq(agents.id, id)).limit(1);
3800
- if (!existing || existing.status === AGENT_STATUSES.DELETED) throw new NotFoundError(`Agent "${id}" not found`);
3801
- if (existing.status !== AGENT_STATUSES.SUSPENDED) throw new BadRequestError("Only suspended agents can be deleted. Active agents are managed by Context Tree.");
3236
+ }).from(agents).where(eq(agents.uuid, uuid)).limit(1);
3237
+ if (!existing || existing.status === AGENT_STATUSES.DELETED) throw new NotFoundError(`Agent "${uuid}" not found`);
3238
+ if (existing.status !== AGENT_STATUSES.SUSPENDED) throw new BadRequestError("Only suspended agents can be deleted. Suspend the agent first.");
3802
3239
  const [agent] = await db.update(agents).set({
3803
3240
  status: AGENT_STATUSES.DELETED,
3241
+ name: null,
3804
3242
  updatedAt: /* @__PURE__ */ new Date()
3805
- }).where(eq(agents.id, id)).returning();
3806
- await db.update(agentTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and(eq(agentTokens.agentId, id), isNull(agentTokens.revokedAt)));
3807
- await db.delete(adapterConfigs).where(eq(adapterConfigs.agentId, id));
3808
- await db.delete(adapterAgentMappings).where(eq(adapterAgentMappings.agentId, id));
3243
+ }).where(eq(agents.uuid, uuid)).returning();
3244
+ await db.update(agentTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and(eq(agentTokens.agentId, uuid), isNull(agentTokens.revokedAt)));
3245
+ await db.delete(adapterConfigs).where(eq(adapterConfigs.agentId, uuid));
3246
+ await db.delete(adapterAgentMappings).where(eq(adapterAgentMappings.agentId, uuid));
3809
3247
  if (!agent) throw new Error("Unexpected: UPDATE RETURNING produced no row");
3810
3248
  return agent;
3811
3249
  }
3250
+ async function bootstrapToken(db, agentName, orgId, githubUsername, options) {
3251
+ let agent;
3252
+ try {
3253
+ agent = await getAgentByName(db, orgId, agentName);
3254
+ } catch (err) {
3255
+ if (err instanceof NotFoundError) {
3256
+ const metadata = {
3257
+ ...options?.metadata,
3258
+ owners: [githubUsername]
3259
+ };
3260
+ agent = await createAgent(db, {
3261
+ name: agentName,
3262
+ type: options?.type ?? "autonomous_agent",
3263
+ displayName: options?.displayName ?? agentName,
3264
+ delegateMention: options?.delegateMention,
3265
+ profile: options?.profile,
3266
+ organizationId: orgId,
3267
+ metadata
3268
+ });
3269
+ } else throw err;
3270
+ }
3271
+ 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 "${agentName}"`);
3272
+ const activeTokens = await db.select({ id: agentTokens.id }).from(agentTokens).where(and(eq(agentTokens.agentId, agent.uuid), isNull(agentTokens.revokedAt)));
3273
+ if (activeTokens.length > 0) throw new ConflictError(`Agent "${agentName}" already has ${activeTokens.length} active token(s). Revoke all tokens first to re-bootstrap.`);
3274
+ return createToken(db, agent.uuid, { name: options?.tokenName ?? "bootstrap" });
3275
+ }
3812
3276
  /**
3813
- * Bootstrap a token for an agent using GitHub identity.
3814
- * Only works when the agent has no active (non-revoked, non-expired) tokens.
3277
+ * Check if a GitHub user belongs to a specific organization.
3815
3278
  */
3816
- async function bootstrapToken(db, agentId, githubUsername, tokenName) {
3817
- const agent = await getAgent(db, agentId);
3818
- 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}"`);
3819
- const activeTokens = await db.select({ id: agentTokens.id }).from(agentTokens).where(and(eq(agentTokens.agentId, agentId), isNull(agentTokens.revokedAt)));
3820
- if (activeTokens.length > 0) throw new ConflictError(`Agent "${agentId}" already has ${activeTokens.length} active token(s). Revoke all tokens first to re-bootstrap.`);
3821
- return createToken(db, agentId, { name: tokenName ?? "bootstrap" });
3822
- }
3823
- async function createToken(db, agentId, data) {
3824
- await getAgent(db, agentId);
3279
+ async function checkGitHubOrgMembership(githubToken, org) {
3280
+ const res = await fetch(`https://api.github.com/user/orgs`, { headers: {
3281
+ Authorization: `Bearer ${githubToken}`,
3282
+ Accept: "application/vnd.github+json"
3283
+ } });
3284
+ if (!res.ok) return false;
3285
+ return (await res.json()).some((o) => o.login.toLowerCase() === org.toLowerCase());
3286
+ }
3287
+ async function createToken(db, agentUuid, data) {
3288
+ await getAgent(db, agentUuid);
3825
3289
  const raw = `aghub_${randomBytes(32).toString("hex")}`;
3826
3290
  const tokenHash = hashToken$1(raw);
3827
- const tokenId = randomUUID();
3291
+ const tokenId = uuidv7();
3828
3292
  const [token] = await db.insert(agentTokens).values({
3829
3293
  id: tokenId,
3830
- agentId,
3294
+ agentId: agentUuid,
3831
3295
  tokenHash,
3832
3296
  name: data.name ?? null,
3833
3297
  expiresAt: data.expiresAt ? new Date(data.expiresAt) : null
@@ -3844,7 +3308,7 @@ async function createToken(db, agentId, data) {
3844
3308
  token: raw
3845
3309
  };
3846
3310
  }
3847
- async function listTokens(db, agentId) {
3311
+ async function listTokens(db, agentUuid) {
3848
3312
  return db.select({
3849
3313
  id: agentTokens.id,
3850
3314
  agentId: agentTokens.agentId,
@@ -3853,10 +3317,10 @@ async function listTokens(db, agentId) {
3853
3317
  revokedAt: agentTokens.revokedAt,
3854
3318
  createdAt: agentTokens.createdAt,
3855
3319
  lastUsedAt: agentTokens.lastUsedAt
3856
- }).from(agentTokens).where(eq(agentTokens.agentId, agentId)).orderBy(desc(agentTokens.createdAt));
3320
+ }).from(agentTokens).where(eq(agentTokens.agentId, agentUuid)).orderBy(desc(agentTokens.createdAt));
3857
3321
  }
3858
- async function revokeToken(db, agentId, tokenId) {
3859
- const [token] = await db.update(agentTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and(eq(agentTokens.id, tokenId), eq(agentTokens.agentId, agentId), isNull(agentTokens.revokedAt))).returning();
3322
+ async function revokeToken(db, agentUuid, tokenId) {
3323
+ const [token] = await db.update(agentTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and(eq(agentTokens.id, tokenId), eq(agentTokens.agentId, agentUuid), isNull(agentTokens.revokedAt))).returning();
3860
3324
  if (!token) throw new NotFoundError("Token not found or already revoked");
3861
3325
  return token;
3862
3326
  }
@@ -3864,9 +3328,9 @@ async function createChat(db, creatorId, data) {
3864
3328
  const chatId = randomUUID();
3865
3329
  const allParticipantIds = new Set([creatorId, ...data.participantIds]);
3866
3330
  const existingAgents = await db.select({
3867
- id: agents.id,
3331
+ id: agents.uuid,
3868
3332
  organizationId: agents.organizationId
3869
- }).from(agents).where(inArray(agents.id, [...allParticipantIds]));
3333
+ }).from(agents).where(inArray(agents.uuid, [...allParticipantIds]));
3870
3334
  if (existingAgents.length !== allParticipantIds.size) {
3871
3335
  const found = new Set(existingAgents.map((a) => a.id));
3872
3336
  throw new BadRequestError(`Agents not found: ${[...allParticipantIds].filter((id) => !found.has(id)).join(", ")}`);
@@ -3935,9 +3399,9 @@ async function addParticipant(db, chatId, requesterId, data) {
3935
3399
  const chat = await getChat(db, chatId);
3936
3400
  await assertParticipant(db, chatId, requesterId);
3937
3401
  const [targetAgent] = await db.select({
3938
- id: agents.id,
3402
+ id: agents.uuid,
3939
3403
  organizationId: agents.organizationId
3940
- }).from(agents).where(eq(agents.id, data.agentId)).limit(1);
3404
+ }).from(agents).where(eq(agents.uuid, data.agentId)).limit(1);
3941
3405
  if (!targetAgent) throw new NotFoundError(`Agent "${data.agentId}" not found`);
3942
3406
  if (targetAgent.organizationId !== chat.organizationId) throw new BadRequestError("Cannot add agent from different organization");
3943
3407
  const [existing] = await db.select({ chatId: chatParticipants.chatId }).from(chatParticipants).where(and(eq(chatParticipants.chatId, chatId), eq(chatParticipants.agentId, data.agentId))).limit(1);
@@ -3965,7 +3429,7 @@ async function findOrCreateDirectChat(db, agentAId, agentBId) {
3965
3429
  const directChats = await db.select().from(chats).where(and(inArray(chats.id, commonChatIds), eq(chats.type, "direct")));
3966
3430
  if (directChats.length > 0 && directChats[0]) return directChats[0];
3967
3431
  }
3968
- const [agentA] = await db.select({ organizationId: agents.organizationId }).from(agents).where(eq(agents.id, agentAId)).limit(1);
3432
+ const [agentA] = await db.select({ organizationId: agents.organizationId }).from(agents).where(eq(agents.uuid, agentAId)).limit(1);
3969
3433
  if (!agentA) throw new NotFoundError(`Agent "${agentAId}" not found`);
3970
3434
  const chatId = randomUUID();
3971
3435
  return db.transaction(async (tx) => {
@@ -4045,7 +3509,7 @@ async function sendMessage(db, chatId, senderId, data) {
4045
3509
  const entries = (await tx.select({
4046
3510
  agentId: chatParticipants.agentId,
4047
3511
  inboxId: agents.inboxId
4048
- }).from(chatParticipants).innerJoin(agents, eq(chatParticipants.agentId, agents.id)).where(eq(chatParticipants.chatId, chatId))).filter((p) => p.agentId !== senderId).map((p) => ({
3512
+ }).from(chatParticipants).innerJoin(agents, eq(chatParticipants.agentId, agents.uuid)).where(eq(chatParticipants.chatId, chatId))).filter((p) => p.agentId !== senderId).map((p) => ({
4049
3513
  inboxId: p.inboxId,
4050
3514
  messageId,
4051
3515
  chatId
@@ -4074,18 +3538,15 @@ async function sendMessage(db, chatId, senderId, data) {
4074
3538
  };
4075
3539
  });
4076
3540
  }
4077
- async function sendToAgent(db, senderId, targetAgentId, data) {
3541
+ async function sendToAgent(db, senderUuid, targetName, data) {
4078
3542
  const [sender] = await db.select({
4079
- id: agents.id,
4080
- organizationId: agents.organizationId
4081
- }).from(agents).where(eq(agents.id, senderId)).limit(1);
4082
- if (!sender) throw new NotFoundError(`Agent "${senderId}" not found`);
4083
- const [target] = await db.select({
4084
- id: agents.id,
3543
+ uuid: agents.uuid,
4085
3544
  organizationId: agents.organizationId
4086
- }).from(agents).where(eq(agents.id, targetAgentId)).limit(1);
4087
- if (!target) throw new NotFoundError(`Agent "${targetAgentId}" not found`);
4088
- return sendMessage(db, (await findOrCreateDirectChat(db, senderId, targetAgentId)).id, senderId, {
3545
+ }).from(agents).where(eq(agents.uuid, senderUuid)).limit(1);
3546
+ if (!sender) throw new NotFoundError(`Agent "${senderUuid}" not found`);
3547
+ const [target] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, sender.organizationId), eq(agents.name, targetName), ne(agents.status, "deleted"))).limit(1);
3548
+ if (!target) throw new NotFoundError(`Agent "${targetName}" not found`);
3549
+ return sendMessage(db, (await findOrCreateDirectChat(db, senderUuid, target.uuid)).id, senderUuid, {
4089
3550
  format: data.format,
4090
3551
  content: data.content,
4091
3552
  metadata: data.metadata,
@@ -4255,9 +3716,12 @@ function serializeDate$1(d) {
4255
3716
  return d ? d.toISOString() : null;
4256
3717
  }
4257
3718
  async function adminAgentRoutes(app) {
3719
+ const listAgentsFilterSchema = z.object({ type: agentTypeSchema.optional() });
4258
3720
  app.get("/", async (request) => {
4259
3721
  const query = paginationQuerySchema.parse(request.query);
4260
- const result = await listAgents(app.db, query.limit, query.cursor);
3722
+ const { type } = listAgentsFilterSchema.parse(request.query);
3723
+ const org = request.query.org ?? "default";
3724
+ const result = await listAgents(app.db, org, query.limit, query.cursor, type);
4261
3725
  return {
4262
3726
  items: result.items.map((a) => ({
4263
3727
  ...a,
@@ -4268,17 +3732,35 @@ async function adminAgentRoutes(app) {
4268
3732
  nextCursor: result.nextCursor
4269
3733
  };
4270
3734
  });
4271
- app.get("/:agentId", async (request) => {
4272
- const agent = await getAgent(app.db, request.params.agentId);
3735
+ app.post("/", async (request, reply) => {
3736
+ const body = createAgentSchema.parse(request.body);
3737
+ const agent = await createAgent(app.db, body);
3738
+ return reply.status(201).send({
3739
+ ...agent,
3740
+ createdAt: agent.createdAt.toISOString(),
3741
+ updatedAt: agent.updatedAt.toISOString()
3742
+ });
3743
+ });
3744
+ app.patch("/:uuid", async (request) => {
3745
+ const body = updateAgentSchema.parse(request.body);
3746
+ const agent = await updateAgent(app.db, request.params.uuid, body);
4273
3747
  return {
4274
3748
  ...agent,
4275
3749
  createdAt: agent.createdAt.toISOString(),
4276
3750
  updatedAt: agent.updatedAt.toISOString()
4277
3751
  };
4278
3752
  });
4279
- app.post("/:agentId/tokens", async (request, reply) => {
3753
+ app.get("/:uuid", async (request) => {
3754
+ const agent = await getAgent(app.db, request.params.uuid);
3755
+ return {
3756
+ ...agent,
3757
+ createdAt: agent.createdAt.toISOString(),
3758
+ updatedAt: agent.updatedAt.toISOString()
3759
+ };
3760
+ });
3761
+ app.post("/:uuid/tokens", async (request, reply) => {
4280
3762
  const body = createAgentTokenSchema.parse(request.body);
4281
- const result = await createToken(app.db, request.params.agentId, body);
3763
+ const result = await createToken(app.db, request.params.uuid, body);
4282
3764
  return reply.status(201).send({
4283
3765
  ...result,
4284
3766
  expiresAt: serializeDate$1(result.expiresAt),
@@ -4287,8 +3769,8 @@ async function adminAgentRoutes(app) {
4287
3769
  lastUsedAt: serializeDate$1(result.lastUsedAt)
4288
3770
  });
4289
3771
  });
4290
- app.get("/:agentId/tokens", async (request) => {
4291
- return (await listTokens(app.db, request.params.agentId)).map((t) => ({
3772
+ app.get("/:uuid/tokens", async (request) => {
3773
+ return (await listTokens(app.db, request.params.uuid)).map((t) => ({
4292
3774
  ...t,
4293
3775
  expiresAt: serializeDate$1(t.expiresAt),
4294
3776
  revokedAt: serializeDate$1(t.revokedAt),
@@ -4296,39 +3778,55 @@ async function adminAgentRoutes(app) {
4296
3778
  lastUsedAt: serializeDate$1(t.lastUsedAt)
4297
3779
  }));
4298
3780
  });
4299
- app.delete("/:agentId/tokens/:tokenId", async (request, reply) => {
4300
- await revokeToken(app.db, request.params.agentId, request.params.tokenId);
3781
+ app.delete("/:uuid/tokens/:tokenId", async (request, reply) => {
3782
+ await revokeToken(app.db, request.params.uuid, request.params.tokenId);
4301
3783
  return reply.status(204).send();
4302
3784
  });
4303
- app.post("/:agentId/disconnect", async (request, reply) => {
4304
- const { agentId } = request.params;
4305
- await getAgent(app.db, agentId);
4306
- const wasConnected = forceDisconnect(agentId);
4307
- await setOffline(app.db, agentId);
3785
+ app.post("/:uuid/disconnect", async (request, reply) => {
3786
+ const { uuid } = request.params;
3787
+ await getAgent(app.db, uuid);
3788
+ const wasConnected = forceDisconnect(uuid);
3789
+ await setOffline(app.db, uuid);
4308
3790
  return reply.status(200).send({ disconnected: wasConnected });
4309
3791
  });
4310
- app.delete("/:agentId", async (request, reply) => {
4311
- await deleteAgent(app.db, request.params.agentId);
3792
+ app.post("/:uuid/suspend", async (request) => {
3793
+ const agent = await suspendAgent(app.db, request.params.uuid);
3794
+ return {
3795
+ ...agent,
3796
+ createdAt: agent.createdAt.toISOString(),
3797
+ updatedAt: agent.updatedAt.toISOString()
3798
+ };
3799
+ });
3800
+ app.post("/:uuid/reactivate", async (request) => {
3801
+ const agent = await reactivateAgent(app.db, request.params.uuid);
3802
+ return {
3803
+ ...agent,
3804
+ createdAt: agent.createdAt.toISOString(),
3805
+ updatedAt: agent.updatedAt.toISOString()
3806
+ };
3807
+ });
3808
+ app.delete("/:uuid", async (request, reply) => {
3809
+ await deleteAgent(app.db, request.params.uuid);
4312
3810
  return reply.status(204).send();
4313
3811
  });
4314
- app.post("/:agentId/test", async (request, reply) => {
4315
- const { agentId } = request.params;
4316
- const [, presence] = await Promise.all([getAgent(app.db, agentId), getPresence(app.db, agentId)]);
3812
+ app.post("/:uuid/test", async (request, reply) => {
3813
+ const { uuid } = request.params;
3814
+ const [, presence] = await Promise.all([getAgent(app.db, uuid), getPresence(app.db, uuid)]);
4317
3815
  if (!presence || presence.status !== "online") return reply.status(200).send({
4318
3816
  status: "offline",
4319
3817
  message: "Agent is not connected. Start the client first."
4320
3818
  });
4321
- const [owner] = await app.db.select({ id: agents.id }).from(agents).where(and(eq(agents.delegateMention, agentId), eq(agents.status, "active"))).limit(1);
4322
- let senderId = owner?.id ?? null;
3819
+ const [owner] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.delegateMention, uuid), eq(agents.status, "active"))).limit(1);
3820
+ let senderId = owner?.uuid ?? null;
4323
3821
  if (!senderId) {
4324
- const [other] = await app.db.select({ id: agents.id }).from(agents).where(and(ne(agents.id, agentId), eq(agents.status, "active"))).limit(1);
4325
- senderId = other?.id ?? null;
3822
+ const [other] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(ne(agents.uuid, uuid), eq(agents.status, "active"))).limit(1);
3823
+ senderId = other?.uuid ?? null;
4326
3824
  }
4327
3825
  if (!senderId) return reply.status(200).send({
4328
3826
  status: "error",
4329
3827
  message: "No suitable sender found. Need at least one other active agent."
4330
3828
  });
4331
- const chat = await findOrCreateDirectChat(app.db, senderId, agentId);
3829
+ const chat = await findOrCreateDirectChat(app.db, senderId, uuid);
4332
3830
  const testContent = `[System Test] Verify your connection. Respond with your identity and role. Time: ${(/* @__PURE__ */ new Date()).toISOString()}`;
4333
3831
  const result = await sendMessage(app.db, chat.id, senderId, {
4334
3832
  format: "text",
@@ -4341,7 +3839,7 @@ async function adminAgentRoutes(app) {
4341
3839
  const pollStart = Date.now();
4342
3840
  while (Date.now() - pollStart < POLL_TIMEOUT) {
4343
3841
  await new Promise((r) => setTimeout(r, POLL_INTERVAL));
4344
- const [response] = await app.db.select().from(messages).where(and(eq(messages.chatId, chat.id), eq(messages.senderId, agentId), gt(messages.createdAt, threshold))).limit(1);
3842
+ const [response] = await app.db.select().from(messages).where(and(eq(messages.chatId, chat.id), eq(messages.senderId, uuid), gt(messages.createdAt, threshold))).limit(1);
4345
3843
  if (response) {
4346
3844
  const content = typeof response.content === "string" ? response.content.slice(0, 500) : JSON.stringify(response.content).slice(0, 500);
4347
3845
  return reply.status(200).send({
@@ -4423,7 +3921,10 @@ async function adminChatRoutes(app) {
4423
3921
  /** List chats with participant count */
4424
3922
  app.get("/", async (request) => {
4425
3923
  const query = paginationQuerySchema.parse(request.query);
4426
- const where = query.cursor ? lt(chats.createdAt, new Date(query.cursor)) : void 0;
3924
+ const org = request.query.org ?? "default";
3925
+ const conditions = [eq(chats.organizationId, org)];
3926
+ if (query.cursor) conditions.push(lt(chats.createdAt, new Date(query.cursor)));
3927
+ const where = and(...conditions);
4427
3928
  const rows = await app.db.select({
4428
3929
  id: chats.id,
4429
3930
  organizationId: chats.organizationId,
@@ -4502,9 +4003,10 @@ async function adminChatRoutes(app) {
4502
4003
  });
4503
4004
  }
4504
4005
  async function adminOverviewRoutes(app) {
4505
- app.get("/", async () => {
4506
- const [agentCount] = await app.db.select({ count: sql`count(*)::int` }).from(agents).where(ne(agents.status, "deleted"));
4507
- const [chatCount] = await app.db.select({ count: sql`count(*)::int` }).from(chats);
4006
+ app.get("/", async (request) => {
4007
+ const org = (request.query ?? {}).org ?? "default";
4008
+ const [agentCount] = await app.db.select({ count: sql`count(*)::int` }).from(agents).where(and(ne(agents.status, "deleted"), eq(agents.organizationId, org)));
4009
+ const [chatCount] = await app.db.select({ count: sql`count(*)::int` }).from(chats).where(eq(chats.organizationId, org));
4508
4010
  const onlineCount = await getOnlineCount(app.db);
4509
4011
  return {
4510
4012
  agents: agentCount?.count ?? 0,
@@ -4657,7 +4159,7 @@ async function agentChatRoutes(app) {
4657
4159
  app.post("/", async (request, reply) => {
4658
4160
  const identity = requireAgent(request);
4659
4161
  const body = createChatSchema.parse(request.body);
4660
- const result = await createChat(app.db, identity.id, body);
4162
+ const result = await createChat(app.db, identity.uuid, body);
4661
4163
  return reply.status(201).send({
4662
4164
  ...serializeChat(result),
4663
4165
  participants: result.participants.map((p) => ({
@@ -4669,7 +4171,7 @@ async function agentChatRoutes(app) {
4669
4171
  app.get("/", async (request) => {
4670
4172
  const identity = requireAgent(request);
4671
4173
  const query = paginationQuerySchema.parse(request.query);
4672
- const result = await listChats(app.db, identity.id, query.limit, query.cursor);
4174
+ const result = await listChats(app.db, identity.uuid, query.limit, query.cursor);
4673
4175
  return {
4674
4176
  items: result.items.map(serializeChat),
4675
4177
  nextCursor: result.nextCursor
@@ -4677,7 +4179,7 @@ async function agentChatRoutes(app) {
4677
4179
  });
4678
4180
  app.get("/:chatId", async (request) => {
4679
4181
  const identity = requireAgent(request);
4680
- await assertParticipant(app.db, request.params.chatId, identity.id);
4182
+ await assertParticipant(app.db, request.params.chatId, identity.uuid);
4681
4183
  const detail = await getChatDetail(app.db, request.params.chatId);
4682
4184
  return {
4683
4185
  ...serializeChat(detail),
@@ -4690,7 +4192,7 @@ async function agentChatRoutes(app) {
4690
4192
  app.post("/:chatId/participants", async (request, reply) => {
4691
4193
  const identity = requireAgent(request);
4692
4194
  const body = addParticipantSchema.parse(request.body);
4693
- const participants = await addParticipant(app.db, request.params.chatId, identity.id, body);
4195
+ const participants = await addParticipant(app.db, request.params.chatId, identity.uuid, body);
4694
4196
  return reply.status(201).send(participants.map((p) => ({
4695
4197
  ...p,
4696
4198
  joinedAt: p.joinedAt.toISOString()
@@ -4698,20 +4200,10 @@ async function agentChatRoutes(app) {
4698
4200
  });
4699
4201
  app.delete("/:chatId/participants/:agentId", async (request, reply) => {
4700
4202
  const identity = requireAgent(request);
4701
- await removeParticipant(app.db, request.params.chatId, identity.id, request.params.agentId);
4203
+ await removeParticipant(app.db, request.params.chatId, identity.uuid, request.params.agentId);
4702
4204
  return reply.status(204).send();
4703
4205
  });
4704
4206
  }
4705
- async function agentContextTreeRoutes(app) {
4706
- app.get("/", async (_request, reply) => {
4707
- const { repo, branch } = app.config.contextTree;
4708
- if (!repo) return reply.status(404).send({ error: "Context Tree not configured" });
4709
- return reply.send({
4710
- repo,
4711
- branch
4712
- });
4713
- });
4714
- }
4715
4207
  async function agentFeishuBotRoutes(app) {
4716
4208
  /**
4717
4209
  * PUT /agent/me/feishu-bot
@@ -4720,8 +4212,8 @@ async function agentFeishuBotRoutes(app) {
4720
4212
  app.put("/me/feishu-bot", async (request, reply) => {
4721
4213
  const identity = requireAgent(request);
4722
4214
  const body = selfServiceFeishuBotSchema.parse(request.body);
4723
- if ((await getAgent(app.db, identity.id)).type === "human") throw new BadRequestError("Human agents cannot bind Feishu bots. Use bind-user instead.");
4724
- const current = (await listAdapterConfigs(app.db)).find((c) => c.agentId === identity.id && c.platform === "feishu");
4215
+ if ((await getAgent(app.db, identity.uuid)).type === "human") throw new BadRequestError("Human agents cannot bind Feishu bots. Use bind-user instead.");
4216
+ const current = (await listAdapterConfigs(app.db)).find((c) => c.agentId === identity.uuid && c.platform === "feishu");
4725
4217
  let config;
4726
4218
  if (current) config = await updateAdapterConfig(app.db, current.id, {
4727
4219
  credentials: {
@@ -4732,7 +4224,7 @@ async function agentFeishuBotRoutes(app) {
4732
4224
  }, app.config.secrets.encryptionKey);
4733
4225
  else config = await createAdapterConfig(app.db, {
4734
4226
  platform: "feishu",
4735
- agentId: identity.id,
4227
+ agentId: identity.uuid,
4736
4228
  credentials: {
4737
4229
  app_id: body.appId,
4738
4230
  app_secret: body.appSecret
@@ -4753,7 +4245,7 @@ async function agentFeishuBotRoutes(app) {
4753
4245
  */
4754
4246
  app.delete("/me/feishu-bot", async (request, reply) => {
4755
4247
  const identity = requireAgent(request);
4756
- const current = (await listAdapterConfigs(app.db)).find((c) => c.agentId === identity.id && c.platform === "feishu");
4248
+ const current = (await listAdapterConfigs(app.db)).find((c) => c.agentId === identity.uuid && c.platform === "feishu");
4757
4249
  if (!current) return reply.status(204).send();
4758
4250
  await deleteAdapterConfig(app.db, current.id);
4759
4251
  app.adapterManager.reload().catch((err) => app.log.error(err, "Adapter reload failed after self-service unbind"));
@@ -4772,7 +4264,7 @@ async function agentFeishuUserRoutes(app) {
4772
4264
  const body = delegateFeishuUserSchema.parse(request.body);
4773
4265
  const humanAgent = await getAgent(app.db, humanAgentId);
4774
4266
  if (humanAgent.type !== "human") throw new BadRequestError(`Agent "${humanAgentId}" is not a human agent`);
4775
- if (humanAgent.delegateMention !== identity.id) throw new ForbiddenError(`Agent "${identity.id}" is not the delegate of "${humanAgentId}". Expected delegate_mention="${identity.id}" but found "${humanAgent.delegateMention ?? "(none)"}".`);
4267
+ if (humanAgent.delegateMention !== identity.uuid) throw new ForbiddenError(`Agent "${identity.uuid}" is not the delegate of "${humanAgentId}". Expected delegate_mention="${identity.uuid}" but found "${humanAgent.delegateMention ?? "(none)"}".`);
4776
4268
  const mapping = await createAgentMapping(app.db, {
4777
4269
  platform: "feishu",
4778
4270
  externalUserId: body.feishuUserId,
@@ -4797,7 +4289,7 @@ async function agentFeishuUserRoutes(app) {
4797
4289
  app.delete("/:humanAgentId/feishu-user", async (request, reply) => {
4798
4290
  const identity = requireAgent(request);
4799
4291
  const { humanAgentId } = request.params;
4800
- if ((await getAgent(app.db, humanAgentId)).delegateMention !== identity.id) throw new ForbiddenError(`Agent "${identity.id}" is not the delegate of "${humanAgentId}"`);
4292
+ if ((await getAgent(app.db, humanAgentId)).delegateMention !== identity.uuid) throw new ForbiddenError(`Agent "${identity.uuid}" is not the delegate of "${humanAgentId}"`);
4801
4293
  await app.db.delete(adapterAgentMappings).where(and(eq(adapterAgentMappings.platform, "feishu"), eq(adapterAgentMappings.agentId, humanAgentId)));
4802
4294
  return reply.status(204).send();
4803
4295
  });
@@ -4906,7 +4398,7 @@ async function agentInboxRoutes(app) {
4906
4398
  async function agentMeRoutes(app) {
4907
4399
  app.get("/me", async (request) => {
4908
4400
  const identity = requireAgent(request);
4909
- const agent = await getAgent(app.db, identity.id);
4401
+ const agent = await getAgent(app.db, identity.uuid);
4910
4402
  return {
4911
4403
  ...agent,
4912
4404
  createdAt: agent.createdAt.toISOString(),
@@ -4921,9 +4413,9 @@ const editMessageSchema = z.object({
4921
4413
  async function agentMessageRoutes(app) {
4922
4414
  app.post("/:chatId/messages", async (request, reply) => {
4923
4415
  const identity = requireAgent(request);
4924
- await assertParticipant(app.db, request.params.chatId, identity.id);
4416
+ await assertParticipant(app.db, request.params.chatId, identity.uuid);
4925
4417
  const body = sendMessageSchema.parse(request.body);
4926
- const { message: msg, recipients } = await sendMessage(app.db, request.params.chatId, identity.id, body);
4418
+ const { message: msg, recipients } = await sendMessage(app.db, request.params.chatId, identity.uuid, body);
4927
4419
  notifyRecipients(app.notifier, recipients, msg.id);
4928
4420
  return reply.status(201).send({
4929
4421
  ...msg,
@@ -4932,9 +4424,9 @@ async function agentMessageRoutes(app) {
4932
4424
  });
4933
4425
  app.patch("/:chatId/messages/:messageId", async (request) => {
4934
4426
  const identity = requireAgent(request);
4935
- await assertParticipant(app.db, request.params.chatId, identity.id);
4427
+ await assertParticipant(app.db, request.params.chatId, identity.uuid);
4936
4428
  const body = editMessageSchema.parse(request.body);
4937
- const msg = await editMessage(app.db, request.params.chatId, request.params.messageId, identity.id, body);
4429
+ const msg = await editMessage(app.db, request.params.chatId, request.params.messageId, identity.uuid, body);
4938
4430
  app.adapterManager.editOutboundMessage(msg.id, msg.format, msg.content).catch((err) => app.log.error({
4939
4431
  err,
4940
4432
  messageId: msg.id
@@ -4946,7 +4438,7 @@ async function agentMessageRoutes(app) {
4946
4438
  });
4947
4439
  app.get("/:chatId/messages", async (request) => {
4948
4440
  const identity = requireAgent(request);
4949
- await assertParticipant(app.db, request.params.chatId, identity.id);
4441
+ await assertParticipant(app.db, request.params.chatId, identity.uuid);
4950
4442
  const query = paginationQuerySchema.parse(request.query);
4951
4443
  const result = await listMessages(app.db, request.params.chatId, query.limit, query.cursor);
4952
4444
  return {
@@ -4959,10 +4451,10 @@ async function agentMessageRoutes(app) {
4959
4451
  });
4960
4452
  }
4961
4453
  async function agentSendToAgentRoutes(app) {
4962
- app.post("/:agentId/messages", async (request, reply) => {
4454
+ app.post("/:name/messages", async (request, reply) => {
4963
4455
  const identity = requireAgent(request);
4964
4456
  const body = sendToAgentSchema.parse(request.body);
4965
- const { message: msg, recipients } = await sendToAgent(app.db, identity.id, request.params.agentId, body);
4457
+ const { message: msg, recipients } = await sendToAgent(app.db, identity.uuid, request.params.name, body);
4966
4458
  notifyRecipients(app.notifier, recipients, msg.id);
4967
4459
  return reply.status(201).send({
4968
4460
  ...msg,
@@ -4978,35 +4470,54 @@ function agentWsRoutes(notifier, instanceId) {
4978
4470
  socket.close(4001, "Unauthorized");
4979
4471
  return;
4980
4472
  }
4981
- if (hasActiveConnection(agent.id)) {
4473
+ if (hasActiveConnection(agent.uuid)) {
4982
4474
  socket.close(WS_CLOSE_ALREADY_CONNECTED, "Agent already connected");
4983
4475
  return;
4984
4476
  }
4985
4477
  const inboxId = agent.inboxId;
4986
- await setOnline(app.db, agent.id, instanceId);
4987
- setConnection(agent.id, socket);
4478
+ await setOnline(app.db, agent.uuid, instanceId);
4479
+ setConnection(agent.uuid, socket);
4988
4480
  notifier.subscribe(inboxId, socket);
4989
4481
  socket.on("close", async () => {
4990
4482
  notifier.unsubscribe(inboxId, socket);
4991
- if (removeConnection(agent.id, socket)) try {
4992
- await setOffline(app.db, agent.id);
4483
+ if (removeConnection(agent.uuid, socket)) try {
4484
+ await setOffline(app.db, agent.uuid);
4993
4485
  } catch {}
4994
4486
  });
4995
4487
  });
4996
4488
  };
4997
4489
  }
4490
+ async function bootstrapConfigRoutes(app) {
4491
+ /** Public endpoint — returns bootstrap prerequisites for CLI auto-discovery. */
4492
+ app.get("/config", async () => {
4493
+ return { allowedOrg: app.config.github.allowedOrg ?? null };
4494
+ });
4495
+ }
4998
4496
  async function bootstrapRoutes(app) {
4999
4497
  /**
5000
- * POST /bootstrap/:agentId/token
4498
+ * POST /bootstrap/:name/token
5001
4499
  * GitHub identity → Agent token.
4500
+ * Auto-creates the agent if it does not exist.
5002
4501
  * Only works when the agent has no active tokens.
5003
4502
  */
5004
- app.post("/:agentId/token", async (request, reply) => {
5005
- const { agentId } = request.params;
4503
+ app.post("/:name/token", async (request, reply) => {
4504
+ const { name } = request.params;
5006
4505
  const githubUser = request.githubUser;
5007
4506
  if (!githubUser) throw new ForbiddenError("GitHub authentication required");
4507
+ const allowedOrg = app.config.github.allowedOrg;
4508
+ if (!allowedOrg) throw new ForbiddenError("FIRST_TREE_HUB_GITHUB_ALLOWED_ORG is not configured. Agent registration is disabled.");
4509
+ const githubToken = request.headers["x-github-token"];
4510
+ if (!githubToken || typeof githubToken !== "string") throw new ForbiddenError("Missing GitHub token for org membership check");
4511
+ if (!await checkGitHubOrgMembership(githubToken, allowedOrg)) throw new ForbiddenError(`GitHub user "${githubUser.username}" is not a member of organization "${allowedOrg}"`);
5008
4512
  const body = bootstrapTokenRequestSchema.parse(request.body ?? {});
5009
- const result = await bootstrapToken(app.db, agentId, githubUser.username, body.name);
4513
+ const result = await bootstrapToken(app.db, name, "default", githubUser.username, {
4514
+ tokenName: body.name,
4515
+ type: body.type,
4516
+ displayName: body.displayName,
4517
+ delegateMention: body.delegateMention,
4518
+ profile: body.profile,
4519
+ metadata: body.metadata
4520
+ });
5010
4521
  return reply.status(201).send({
5011
4522
  id: result.id,
5012
4523
  agentId: result.agentId,
@@ -5017,16 +4528,16 @@ async function bootstrapRoutes(app) {
5017
4528
  });
5018
4529
  });
5019
4530
  /**
5020
- * GET /bootstrap/:agentId/status
5021
- * Check if an agent exists and its status (for polling after PR merge + sync).
4531
+ * GET /bootstrap/:name/status
4532
+ * Check if an agent exists and its status (for polling).
5022
4533
  */
5023
- app.get("/:agentId/status", async (request) => {
5024
- const { agentId } = request.params;
4534
+ app.get("/:name/status", async (request) => {
4535
+ const { name } = request.params;
5025
4536
  const githubUser = request.githubUser;
5026
4537
  if (!githubUser) throw new ForbiddenError("GitHub authentication required");
5027
4538
  try {
5028
- const agent = await getAgent(app.db, agentId);
5029
- if (!(Array.isArray(agent.metadata?.owners) ? agent.metadata.owners : []).includes(githubUser.username)) throw new ForbiddenError(`GitHub user "${githubUser.username}" is not in the owners list for agent "${agentId}"`);
4539
+ const agent = await getAgentByName(app.db, "default", name);
4540
+ if (!(Array.isArray(agent.metadata?.owners) ? agent.metadata.owners : []).includes(githubUser.username)) throw new ForbiddenError(`GitHub user "${githubUser.username}" is not in the owners list for agent "${name}"`);
5030
4541
  return {
5031
4542
  exists: true,
5032
4543
  status: agent.status
@@ -5043,11 +4554,9 @@ async function bootstrapRoutes(app) {
5043
4554
  async function contextTreeInfoRoutes(app) {
5044
4555
  /** Public endpoint — returns Context Tree repo metadata for CLI auto-discovery. */
5045
4556
  app.get("/info", async () => {
5046
- const { repo, branch } = app.config.contextTree;
5047
4557
  return {
5048
- repo,
5049
- branch,
5050
- lastSync: null
4558
+ repo: app.config.contextTree?.repo ?? null,
4559
+ branch: app.config.contextTree?.branch ?? null
5051
4560
  };
5052
4561
  });
5053
4562
  }
@@ -5093,34 +4602,35 @@ function verifySignature(secret, rawBody, signatureHeader) {
5093
4602
  if (expectedBuf.length !== receivedBuf.length || !timingSafeEqual(expectedBuf, receivedBuf)) throw new UnauthorizedError("Invalid webhook signature");
5094
4603
  }
5095
4604
  async function ensureGitHubAdapterAgent(db) {
5096
- const [existing] = await db.select({ id: agents.id }).from(agents).where(eq(agents.id, GITHUB_ADAPTER_ID)).limit(1);
5097
- if (existing) return existing.id;
4605
+ const [existing] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, "default"), eq(agents.name, GITHUB_ADAPTER_ID))).limit(1);
4606
+ if (existing) return existing.uuid;
5098
4607
  try {
5099
4608
  return (await createAgent(db, {
5100
- id: GITHUB_ADAPTER_ID,
4609
+ name: GITHUB_ADAPTER_ID,
5101
4610
  type: "autonomous_agent",
5102
4611
  displayName: "GitHub Adapter",
5103
4612
  metadata: {
5104
4613
  source: "github",
5105
4614
  managed: true
5106
4615
  }
5107
- })).id;
4616
+ })).uuid;
5108
4617
  } catch (err) {
5109
4618
  if (err instanceof ConflictError) {
5110
- const [created] = await db.select({ id: agents.id }).from(agents).where(eq(agents.id, GITHUB_ADAPTER_ID)).limit(1);
5111
- if (created) return created.id;
4619
+ const [created] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, "default"), eq(agents.name, GITHUB_ADAPTER_ID))).limit(1);
4620
+ if (created) return created.uuid;
5112
4621
  }
5113
4622
  throw err;
5114
4623
  }
5115
4624
  }
5116
4625
  async function findTargetAgent(db, repoFullName) {
5117
4626
  const allAgents = await db.select({
5118
- id: agents.id,
4627
+ id: agents.uuid,
4628
+ name: agents.name,
5119
4629
  metadata: agents.metadata,
5120
4630
  type: agents.type
5121
4631
  }).from(agents).where(eq(agents.status, "active"));
5122
4632
  for (const agent of allAgents) {
5123
- if (agent.id === GITHUB_ADAPTER_ID) continue;
4633
+ if (agent.name === GITHUB_ADAPTER_ID) continue;
5124
4634
  const meta = agent.metadata;
5125
4635
  if (meta && typeof meta === "object" && "github" in meta) {
5126
4636
  const github = meta.github;
@@ -5155,27 +4665,29 @@ function extractMentions(text) {
5155
4665
  async function routeMentionDelegations(app, mentionedNames, ctx) {
5156
4666
  if (mentionedNames.length === 0) return 0;
5157
4667
  const delegates = await app.db.select({
5158
- id: agents.id,
4668
+ id: agents.uuid,
4669
+ name: agents.name,
5159
4670
  delegateMention: agents.delegateMention,
5160
4671
  status: agents.status
5161
- }).from(agents).where(and(inArray(agents.id, mentionedNames), isNotNull(agents.delegateMention)));
4672
+ }).from(agents).where(and(inArray(agents.name, mentionedNames), isNotNull(agents.delegateMention)));
5162
4673
  let routed = 0;
5163
4674
  for (const agent of delegates) {
5164
4675
  if (agent.status !== "active" || !agent.delegateMention) continue;
5165
4676
  const [target] = await app.db.select({
5166
- id: agents.id,
4677
+ id: agents.uuid,
5167
4678
  status: agents.status
5168
- }).from(agents).where(eq(agents.id, agent.delegateMention)).limit(1);
4679
+ }).from(agents).where(eq(agents.uuid, agent.delegateMention)).limit(1);
5169
4680
  if (!target || target.status !== "active") {
5170
- app.log.warn(`delegate_mention target "${agent.delegateMention}" for "${agent.id}" is not active, skipping`);
4681
+ app.log.warn(`delegate_mention target "${agent.delegateMention}" for "${agent.name}" is not active, skipping`);
5171
4682
  continue;
5172
4683
  }
5173
4684
  try {
5174
- const { message: msg, recipients } = await sendToAgent(app.db, agent.id, agent.delegateMention, {
4685
+ const chat = await findOrCreateDirectChat(app.db, agent.id, agent.delegateMention);
4686
+ const { message: msg, recipients } = await sendMessage(app.db, chat.id, agent.id, {
5175
4687
  format: "card",
5176
4688
  content: {
5177
4689
  type: "github_mention",
5178
- mentionedUser: agent.id,
4690
+ mentionedUser: agent.name,
5179
4691
  event: ctx.event,
5180
4692
  repository: ctx.repository,
5181
4693
  sender: ctx.sender,
@@ -5186,13 +4698,13 @@ async function routeMentionDelegations(app, mentionedNames, ctx) {
5186
4698
  metadata: {
5187
4699
  source: "github",
5188
4700
  event: "mention_delegation",
5189
- mentionedUser: agent.id
4701
+ mentionedUser: agent.name
5190
4702
  }
5191
4703
  });
5192
4704
  notifyRecipients(app.notifier, recipients, msg.id);
5193
4705
  routed++;
5194
4706
  } catch (err) {
5195
- app.log.error(err, `Failed to route mention delegation from "${agent.id}" to "${agent.delegateMention}"`);
4707
+ app.log.error(err, `Failed to route mention delegation from "${agent.name}" to "${agent.delegateMention}"`);
5196
4708
  }
5197
4709
  }
5198
4710
  return routed;
@@ -5480,7 +4992,8 @@ async function handleIssuesEvent(app, eventType, payload, reply) {
5480
4992
  event: "issues",
5481
4993
  action: data.action
5482
4994
  };
5483
- const { message: msg, recipients } = await sendToAgent(app.db, senderId, targetAgentId, {
4995
+ const chat = await findOrCreateDirectChat(app.db, senderId, targetAgentId);
4996
+ const { message: msg, recipients } = await sendMessage(app.db, chat.id, senderId, {
5484
4997
  format: "card",
5485
4998
  content,
5486
4999
  metadata
@@ -5535,7 +5048,8 @@ async function handleIssueCommentEvent(app, eventType, payload, reply) {
5535
5048
  event: "issue_comment",
5536
5049
  action: data.action
5537
5050
  };
5538
- const { message: msg, recipients } = await sendToAgent(app.db, senderId, targetAgentId, {
5051
+ const chat = await findOrCreateDirectChat(app.db, senderId, targetAgentId);
5052
+ const { message: msg, recipients } = await sendMessage(app.db, chat.id, senderId, {
5539
5053
  format: "card",
5540
5054
  content,
5541
5055
  metadata
@@ -5609,10 +5123,11 @@ function agentAuthHook(db) {
5609
5123
  const [tokenDetail] = await db.select({ expiresAt: agentTokens.expiresAt }).from(agentTokens).where(eq(agentTokens.id, tokenRow.tokenId)).limit(1);
5610
5124
  if (tokenDetail?.expiresAt && tokenDetail.expiresAt < now) throw new UnauthorizedError("Token has expired");
5611
5125
  const [agent] = await db.select({
5612
- id: agents.id,
5126
+ uuid: agents.uuid,
5127
+ name: agents.name,
5613
5128
  organizationId: agents.organizationId,
5614
5129
  inboxId: agents.inboxId
5615
- }).from(agents).where(and(eq(agents.id, tokenRow.agentId), eq(agents.status, "active"))).limit(1);
5130
+ }).from(agents).where(and(eq(agents.uuid, tokenRow.agentId), eq(agents.status, "active"))).limit(1);
5616
5131
  if (!agent) throw new UnauthorizedError("Agent is suspended or not found");
5617
5132
  db.update(agentTokens).set({ lastUsedAt: now }).where(eq(agentTokens.id, tokenRow.tokenId)).then(() => {}, () => {});
5618
5133
  request.agent = agent;
@@ -5971,10 +5486,10 @@ async function handleBindCommand(db, bot, event, agentId, log) {
5971
5486
  return;
5972
5487
  }
5973
5488
  const [agent] = await db.select({
5974
- id: agents.id,
5489
+ id: agents.uuid,
5975
5490
  type: agents.type,
5976
5491
  status: agents.status
5977
- }).from(agents).where(eq(agents.id, agentId)).limit(1);
5492
+ }).from(agents).where(eq(agents.uuid, agentId)).limit(1);
5978
5493
  if (!agent) {
5979
5494
  await reply(`Agent "${agentId}" not found. Check the ID and try again.`);
5980
5495
  return;
@@ -6030,7 +5545,7 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
6030
5545
  WHERE id IN (
6031
5546
  SELECT ie.id FROM inbox_entries ie
6032
5547
  JOIN agents a ON ie.inbox_id = a.inbox_id
6033
- JOIN adapter_agent_mappings aam ON a.id = aam.agent_id
5548
+ JOIN adapter_agent_mappings aam ON a.uuid = aam.agent_id
6034
5549
  WHERE aam.platform = 'feishu' AND ie.status = 'pending'
6035
5550
  ORDER BY ie.created_at
6036
5551
  LIMIT ${OUTBOUND_BATCH_SIZE$1}
@@ -6221,8 +5736,8 @@ function createKaelRuntime(db, encryptionKey, kaelEndpoint, kaelApiKey, serverUr
6221
5736
  }
6222
5737
  const configs = await db.select().from(adapterConfigs).where(and(eq(adapterConfigs.platform, "kael"), eq(adapterConfigs.status, "active")));
6223
5738
  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]));
5739
+ const agentRows = configAgentIds.length > 0 ? await db.execute(sql`SELECT uuid, inbox_id FROM agents WHERE uuid IN (${sql.join(configAgentIds.map((id) => sql`${id}`), sql`, `)}) AND status = 'active'`) : [];
5740
+ const agentInboxMap = new Map(agentRows.map((a) => [a.uuid, a.inbox_id]));
6226
5741
  const seen = /* @__PURE__ */ new Set();
6227
5742
  for (const config of configs) {
6228
5743
  if (!config.credentials) continue;
@@ -6282,10 +5797,10 @@ function createKaelRuntime(db, encryptionKey, kaelEndpoint, kaelApiKey, serverUr
6282
5797
  WHERE id IN (
6283
5798
  SELECT ie.id FROM inbox_entries ie
6284
5799
  JOIN agents a ON ie.inbox_id = a.inbox_id
6285
- JOIN adapter_configs ac ON a.id = ac.agent_id
5800
+ JOIN adapter_configs ac ON a.uuid = ac.agent_id
6286
5801
  WHERE ac.platform = 'kael' AND ac.status = 'active'
6287
5802
  AND ie.status = 'pending'
6288
- AND a.id IN (${sql.join(agentIds.map((id) => sql`${id}`), sql`, `)})
5803
+ AND a.uuid IN (${sql.join(agentIds.map((id) => sql`${id}`), sql`, `)})
6289
5804
  ORDER BY ie.created_at
6290
5805
  LIMIT ${OUTBOUND_BATCH_SIZE}
6291
5806
  FOR UPDATE OF ie SKIP LOCKED
@@ -6420,6 +5935,7 @@ async function buildApp(config) {
6420
5935
  await api.register(githubWebhookRoutes, { prefix: "/webhooks" });
6421
5936
  await api.register(adminAuthRoutes, { prefix: "/admin/auth" });
6422
5937
  await api.register(contextTreeInfoRoutes, { prefix: "/context-tree" });
5938
+ await api.register(bootstrapConfigRoutes, { prefix: "/bootstrap" });
6423
5939
  await api.register(async (bootstrapApp) => {
6424
5940
  bootstrapApp.addHook("onRequest", githubAuth);
6425
5941
  await bootstrapApp.register(bootstrapRoutes);
@@ -6428,10 +5944,6 @@ async function buildApp(config) {
6428
5944
  adminApp.addHook("onRequest", adminAuth);
6429
5945
  await adminApp.register(adminAgentRoutes);
6430
5946
  }, { prefix: "/admin/agents" });
6431
- await api.register(async (adminApp) => {
6432
- adminApp.addHook("onRequest", adminAuth);
6433
- await adminApp.register(adminAgentSyncRoutes);
6434
- }, { prefix: "/admin/agents/sync" });
6435
5947
  await api.register(async (adminApp) => {
6436
5948
  adminApp.addHook("onRequest", adminAuth);
6437
5949
  await adminApp.register(adminSystemConfigRoutes);
@@ -6467,7 +5979,6 @@ async function buildApp(config) {
6467
5979
  await agentApp.register(agentMessageRoutes, { prefix: "/chats" });
6468
5980
  await agentApp.register(agentSendToAgentRoutes, { prefix: "/agents" });
6469
5981
  await agentApp.register(agentInboxRoutes, { prefix: "/inbox" });
6470
- await agentApp.register(agentContextTreeRoutes, { prefix: "/context-tree" });
6471
5982
  await agentApp.register(agentFeishuBotRoutes);
6472
5983
  await agentApp.register(agentFeishuUserRoutes, { prefix: "/delegated" });
6473
5984
  await agentApp.register(agentWsRoutes(notifier, config.instanceId), { prefix: "/ws" });
@@ -6503,19 +6014,6 @@ async function buildApp(config) {
6503
6014
  backgroundTasks.start();
6504
6015
  await adapterManager.reload();
6505
6016
  await kaelRuntime?.reload();
6506
- const { repo: ctRepo, branch: ctBranch, syncInterval } = config.contextTree;
6507
- const { token: ghToken } = config.github;
6508
- try {
6509
- const report = await syncFromGitHub(db, ctRepo, ctBranch, ghToken);
6510
- app.log.info(`Context Tree sync: created=${report.created} updated=${report.updated} unchanged=${report.unchanged} errors=${report.errors}`);
6511
- } catch (err) {
6512
- app.log.error(err, "Initial Context Tree sync failed");
6513
- }
6514
- const intervalMs = syncInterval * 1e3;
6515
- const timer = setInterval(() => {
6516
- syncFromGitHub(db, ctRepo, ctBranch, ghToken).catch((err) => app.log.error(err, "Periodic Context Tree sync failed"));
6517
- }, intervalMs);
6518
- app.addHook("onClose", async () => clearInterval(timer));
6519
6017
  });
6520
6018
  app.addHook("onClose", async () => {
6521
6019
  backgroundTasks.stop();
@@ -6635,4 +6133,4 @@ function resolveWebDist() {
6635
6133
  } catch {}
6636
6134
  }
6637
6135
  //#endregion
6638
- export { ClientRuntime as A, checkWebSocket as C, ensurePostgres as D, status as E, SessionRegistry as F, cleanWorkspaces as I, hasAdminUser as M, FirstTreeHubSDK as N, isDockerAvailable as O, SdkError as P, checkServerReachable as S, blank as T, checkDocker as _, formatCheckReport as a, checkServerConfig as b, onboardContinue as c, runMigrations as d, checkAgentConfigs as f, checkDatabase as g, checkContextTreeRepo as h, promptMissingFields as i, createAdminUser$1 as j, stopPostgres as k, onboardCreate as l, checkClientConfig as m, isInteractive as n, loadOnboardState as o, checkAgentTokens as p, promptAddAgent as r, onboardCheck as s, startServer as t, saveOnboardState as u, checkGitHubToken as v, printResults as w, checkServerHealth as x, checkNodeVersion as y };
6136
+ 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 };