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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { 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-uyPaaI05.mjs";
2
2
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { EventEmitter } from "node:events";
@@ -51,12 +51,13 @@ var FirstTreeHubSDK = class {
51
51
  displayName: agent.displayName,
52
52
  type: agent.type,
53
53
  delegateMention: agent.delegateMention ?? null,
54
+ profile: agent.profile ?? null,
54
55
  metadata: agent.metadata ?? {}
55
56
  };
56
57
  }
57
- /** Fetch Context Tree configuration from the server. */
58
+ /** Fetch Context Tree configuration from the server (public endpoint). */
58
59
  async getContextTreeConfig() {
59
- return this.requestJson("/api/v1/agent/context-tree");
60
+ return this.requestJson("/api/v1/context-tree/info");
60
61
  }
61
62
  /** Fetch pending inbox entries. */
62
63
  async pull(limit = 10) {
@@ -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"
2077
- });
2078
- }
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)"
2083
+ hint: "Provide via --server, FIRST_TREE_HUB_SERVER, or config"
2093
2084
  });
2094
2085
  }
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
- });
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
2103
  });
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,30 +2407,21 @@ const AGENT_STATUSES = {
2750
2407
  DELETED: "deleted"
2751
2408
  };
2752
2409
  z.enum(["active", "suspended"]);
2753
- z.object({
2410
+ const createAgentSchema = z.object({
2754
2411
  id: 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
2427
  id: z.string(),
@@ -2781,7 +2429,7 @@ z.object({
2781
2429
  type: agentTypeSchema,
2782
2430
  displayName: z.string().nullable(),
2783
2431
  delegateMention: z.string().nullable(),
2784
- treePath: z.string().nullable(),
2432
+ profile: z.string().nullable(),
2785
2433
  inboxId: z.string(),
2786
2434
  status: z.string(),
2787
2435
  metadata: z.record(z.string(), z.unknown()),
@@ -2789,15 +2437,21 @@ z.object({
2789
2437
  createdAt: z.string(),
2790
2438
  updatedAt: z.string()
2791
2439
  });
2792
- const bootstrapTokenRequestSchema = z.object({ name: z.string().max(100).optional() });
2440
+ const bootstrapTokenRequestSchema = z.object({
2441
+ name: z.string().max(100).optional(),
2442
+ type: agentTypeSchema.optional(),
2443
+ displayName: z.string().max(200).optional(),
2444
+ delegateMention: z.string().max(100).optional(),
2445
+ profile: z.string().optional(),
2446
+ metadata: z.record(z.string(), z.unknown()).optional()
2447
+ });
2793
2448
  z.object({
2794
2449
  exists: z.boolean(),
2795
2450
  status: z.enum(["active", "suspended"]).nullable()
2796
2451
  });
2797
2452
  z.object({
2798
- repo: z.string(),
2799
- branch: z.string(),
2800
- lastSync: z.string().nullable()
2453
+ repo: z.string().nullable(),
2454
+ branch: z.string().nullable()
2801
2455
  });
2802
2456
  const createAgentTokenSchema = z.object({
2803
2457
  name: z.string().max(100).optional(),
@@ -2919,7 +2573,7 @@ const SYSTEM_CONFIG_DEFAULTS = {
2919
2573
  [SYSTEM_CONFIG_KEYS.PRESENCE_CLEANUP_SECONDS]: 60
2920
2574
  };
2921
2575
  //#endregion
2922
- //#region ../server/dist/app-CurdzcN2.mjs
2576
+ //#region ../server/dist/app-41WnR_ri.mjs
2923
2577
  var __defProp = Object.defineProperty;
2924
2578
  var __exportAll = (all, no_symbols) => {
2925
2579
  let target = {};
@@ -2937,7 +2591,7 @@ const agents = pgTable("agents", {
2937
2591
  type: text("type").notNull(),
2938
2592
  displayName: text("display_name"),
2939
2593
  delegateMention: text("delegate_mention"),
2940
- treePath: text("tree_path"),
2594
+ profile: text("profile"),
2941
2595
  inboxId: text("inbox_id").unique().notNull(),
2942
2596
  status: text("status").notNull().default("active"),
2943
2597
  metadata: jsonb("metadata").$type().notNull().default({}),
@@ -3423,289 +3077,6 @@ async function adminAdapterRoutes(app) {
3423
3077
  return reply.status(204).send();
3424
3078
  });
3425
3079
  }
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
3080
  /** Agent online status. Tracked via WebSocket connections; stale entries are cleaned up using server_instances heartbeat. */
3710
3081
  const agentPresence = pgTable("agent_presence", {
3711
3082
  agentId: text("agent_id").primaryKey().references(() => agents.id, { onDelete: "cascade" }),
@@ -3741,6 +3112,8 @@ async function createAgent(db, data) {
3741
3112
  organizationId: data.organizationId ?? "default",
3742
3113
  type: data.type,
3743
3114
  displayName: data.displayName ?? null,
3115
+ delegateMention: data.delegateMention ?? null,
3116
+ profile: data.profile ?? null,
3744
3117
  status: "active",
3745
3118
  metadata: data.metadata ?? {},
3746
3119
  updatedAt: /* @__PURE__ */ new Date()
@@ -3753,6 +3126,8 @@ async function createAgent(db, data) {
3753
3126
  organizationId: data.organizationId ?? "default",
3754
3127
  type: data.type,
3755
3128
  displayName: data.displayName ?? null,
3129
+ delegateMention: data.delegateMention ?? null,
3130
+ profile: data.profile ?? null,
3756
3131
  inboxId,
3757
3132
  metadata: data.metadata ?? {}
3758
3133
  }).returning();
@@ -3764,16 +3139,18 @@ async function getAgent(db, id) {
3764
3139
  if (!agent) throw new NotFoundError(`Agent "${id}" not found`);
3765
3140
  return agent;
3766
3141
  }
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;
3142
+ async function listAgents(db, limit, cursor, type) {
3143
+ const conditions = [ne(agents.status, AGENT_STATUSES.DELETED)];
3144
+ if (cursor) conditions.push(lt(agents.createdAt, new Date(cursor)));
3145
+ if (type) conditions.push(eq(agents.type, type));
3146
+ const where = and(...conditions);
3770
3147
  const rows = await db.select({
3771
3148
  id: agents.id,
3772
3149
  organizationId: agents.organizationId,
3773
3150
  type: agents.type,
3774
3151
  displayName: agents.displayName,
3775
3152
  delegateMention: agents.delegateMention,
3776
- treePath: agents.treePath,
3153
+ profile: agents.profile,
3777
3154
  inboxId: agents.inboxId,
3778
3155
  status: agents.status,
3779
3156
  metadata: agents.metadata,
@@ -3789,8 +3166,50 @@ async function listAgents(db, limit, cursor) {
3789
3166
  nextCursor: hasMore && last ? last.createdAt.toISOString() : null
3790
3167
  };
3791
3168
  }
3169
+ async function updateAgent(db, id, data) {
3170
+ const agent = await getAgent(db, id);
3171
+ const updates = { updatedAt: /* @__PURE__ */ new Date() };
3172
+ if (data.type !== void 0) updates.type = data.type;
3173
+ if (data.displayName !== void 0) updates.displayName = data.displayName;
3174
+ if (data.delegateMention !== void 0) updates.delegateMention = data.delegateMention;
3175
+ if (data.profile !== void 0) updates.profile = data.profile;
3176
+ if (data.metadata !== void 0) updates.metadata = data.metadata;
3177
+ const [updated] = await db.update(agents).set(updates).where(eq(agents.id, agent.id)).returning();
3178
+ if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
3179
+ return updated;
3180
+ }
3181
+ /**
3182
+ * Reactivate a suspended agent.
3183
+ */
3184
+ async function reactivateAgent(db, id) {
3185
+ const [existing] = await db.select({
3186
+ id: agents.id,
3187
+ status: agents.status
3188
+ }).from(agents).where(eq(agents.id, id)).limit(1);
3189
+ if (!existing || existing.status === AGENT_STATUSES.DELETED) throw new NotFoundError(`Agent "${id}" not found`);
3190
+ if (existing.status !== AGENT_STATUSES.SUSPENDED) throw new BadRequestError("Only suspended agents can be reactivated.");
3191
+ const [agent] = await db.update(agents).set({
3192
+ status: AGENT_STATUSES.ACTIVE,
3193
+ updatedAt: /* @__PURE__ */ new Date()
3194
+ }).where(eq(agents.id, id)).returning();
3195
+ if (!agent) throw new Error("Unexpected: UPDATE RETURNING produced no row");
3196
+ return agent;
3197
+ }
3198
+ /**
3199
+ * Suspend an agent. Revokes all active tokens so the agent can no longer authenticate.
3200
+ */
3201
+ async function suspendAgent(db, id) {
3202
+ const [agent] = await db.update(agents).set({
3203
+ status: AGENT_STATUSES.SUSPENDED,
3204
+ updatedAt: /* @__PURE__ */ new Date()
3205
+ }).where(and(eq(agents.id, id), ne(agents.status, AGENT_STATUSES.DELETED))).returning();
3206
+ if (!agent) throw new NotFoundError(`Agent "${id}" not found`);
3207
+ await db.update(agentTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and(eq(agentTokens.agentId, id), isNull(agentTokens.revokedAt)));
3208
+ return agent;
3209
+ }
3792
3210
  /**
3793
- * Delete an agent. Only allowed when status is "suspended" (removed from tree).
3211
+ * Delete an agent. Only allowed when status is "suspended".
3212
+ * Suspend the agent first to revoke tokens, then delete.
3794
3213
  */
3795
3214
  async function deleteAgent(db, id) {
3796
3215
  const [existing] = await db.select({
@@ -3798,7 +3217,7 @@ async function deleteAgent(db, id) {
3798
3217
  status: agents.status
3799
3218
  }).from(agents).where(eq(agents.id, id)).limit(1);
3800
3219
  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.");
3220
+ if (existing.status !== AGENT_STATUSES.SUSPENDED) throw new BadRequestError("Only suspended agents can be deleted. Suspend the agent first.");
3802
3221
  const [agent] = await db.update(agents).set({
3803
3222
  status: AGENT_STATUSES.DELETED,
3804
3223
  updatedAt: /* @__PURE__ */ new Date()
@@ -3809,16 +3228,41 @@ async function deleteAgent(db, id) {
3809
3228
  if (!agent) throw new Error("Unexpected: UPDATE RETURNING produced no row");
3810
3229
  return agent;
3811
3230
  }
3812
- /**
3813
- * Bootstrap a token for an agent using GitHub identity.
3814
- * Only works when the agent has no active (non-revoked, non-expired) tokens.
3815
- */
3816
- async function bootstrapToken(db, agentId, githubUsername, tokenName) {
3817
- const agent = await getAgent(db, agentId);
3231
+ async function bootstrapToken(db, agentId, githubUsername, options) {
3232
+ let agent;
3233
+ try {
3234
+ agent = await getAgent(db, agentId);
3235
+ } catch (err) {
3236
+ if (err instanceof NotFoundError) {
3237
+ const metadata = {
3238
+ ...options?.metadata,
3239
+ owners: [githubUsername]
3240
+ };
3241
+ agent = await createAgent(db, {
3242
+ id: agentId,
3243
+ type: options?.type ?? "autonomous_agent",
3244
+ displayName: options?.displayName ?? agentId,
3245
+ delegateMention: options?.delegateMention,
3246
+ profile: options?.profile,
3247
+ metadata
3248
+ });
3249
+ } else throw err;
3250
+ }
3818
3251
  if (!(Array.isArray(agent.metadata?.owners) ? agent.metadata.owners : []).includes(githubUsername)) throw new ForbiddenError(`GitHub user "${githubUsername}" is not in the owners list for agent "${agentId}"`);
3819
3252
  const activeTokens = await db.select({ id: agentTokens.id }).from(agentTokens).where(and(eq(agentTokens.agentId, agentId), isNull(agentTokens.revokedAt)));
3820
3253
  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" });
3254
+ return createToken(db, agentId, { name: options?.tokenName ?? "bootstrap" });
3255
+ }
3256
+ /**
3257
+ * Check if a GitHub user belongs to a specific organization.
3258
+ */
3259
+ async function checkGitHubOrgMembership(githubToken, org) {
3260
+ const res = await fetch(`https://api.github.com/user/orgs`, { headers: {
3261
+ Authorization: `Bearer ${githubToken}`,
3262
+ Accept: "application/vnd.github+json"
3263
+ } });
3264
+ if (!res.ok) return false;
3265
+ return (await res.json()).some((o) => o.login.toLowerCase() === org.toLowerCase());
3822
3266
  }
3823
3267
  async function createToken(db, agentId, data) {
3824
3268
  await getAgent(db, agentId);
@@ -4255,9 +3699,11 @@ function serializeDate$1(d) {
4255
3699
  return d ? d.toISOString() : null;
4256
3700
  }
4257
3701
  async function adminAgentRoutes(app) {
3702
+ const listAgentsFilterSchema = z.object({ type: agentTypeSchema.optional() });
4258
3703
  app.get("/", async (request) => {
4259
3704
  const query = paginationQuerySchema.parse(request.query);
4260
- const result = await listAgents(app.db, query.limit, query.cursor);
3705
+ const { type } = listAgentsFilterSchema.parse(request.query);
3706
+ const result = await listAgents(app.db, query.limit, query.cursor, type);
4261
3707
  return {
4262
3708
  items: result.items.map((a) => ({
4263
3709
  ...a,
@@ -4268,6 +3714,24 @@ async function adminAgentRoutes(app) {
4268
3714
  nextCursor: result.nextCursor
4269
3715
  };
4270
3716
  });
3717
+ app.post("/", async (request, reply) => {
3718
+ const body = createAgentSchema.parse(request.body);
3719
+ const agent = await createAgent(app.db, body);
3720
+ return reply.status(201).send({
3721
+ ...agent,
3722
+ createdAt: agent.createdAt.toISOString(),
3723
+ updatedAt: agent.updatedAt.toISOString()
3724
+ });
3725
+ });
3726
+ app.patch("/:agentId", async (request) => {
3727
+ const body = updateAgentSchema.parse(request.body);
3728
+ const agent = await updateAgent(app.db, request.params.agentId, body);
3729
+ return {
3730
+ ...agent,
3731
+ createdAt: agent.createdAt.toISOString(),
3732
+ updatedAt: agent.updatedAt.toISOString()
3733
+ };
3734
+ });
4271
3735
  app.get("/:agentId", async (request) => {
4272
3736
  const agent = await getAgent(app.db, request.params.agentId);
4273
3737
  return {
@@ -4307,6 +3771,22 @@ async function adminAgentRoutes(app) {
4307
3771
  await setOffline(app.db, agentId);
4308
3772
  return reply.status(200).send({ disconnected: wasConnected });
4309
3773
  });
3774
+ app.post("/:agentId/suspend", async (request) => {
3775
+ const agent = await suspendAgent(app.db, request.params.agentId);
3776
+ return {
3777
+ ...agent,
3778
+ createdAt: agent.createdAt.toISOString(),
3779
+ updatedAt: agent.updatedAt.toISOString()
3780
+ };
3781
+ });
3782
+ app.post("/:agentId/reactivate", async (request) => {
3783
+ const agent = await reactivateAgent(app.db, request.params.agentId);
3784
+ return {
3785
+ ...agent,
3786
+ createdAt: agent.createdAt.toISOString(),
3787
+ updatedAt: agent.updatedAt.toISOString()
3788
+ };
3789
+ });
4310
3790
  app.delete("/:agentId", async (request, reply) => {
4311
3791
  await deleteAgent(app.db, request.params.agentId);
4312
3792
  return reply.status(204).send();
@@ -4702,16 +4182,6 @@ async function agentChatRoutes(app) {
4702
4182
  return reply.status(204).send();
4703
4183
  });
4704
4184
  }
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
4185
  async function agentFeishuBotRoutes(app) {
4716
4186
  /**
4717
4187
  * PUT /agent/me/feishu-bot
@@ -4995,18 +4465,37 @@ function agentWsRoutes(notifier, instanceId) {
4995
4465
  });
4996
4466
  };
4997
4467
  }
4468
+ async function bootstrapConfigRoutes(app) {
4469
+ /** Public endpoint — returns bootstrap prerequisites for CLI auto-discovery. */
4470
+ app.get("/config", async () => {
4471
+ return { allowedOrg: app.config.github.allowedOrg ?? null };
4472
+ });
4473
+ }
4998
4474
  async function bootstrapRoutes(app) {
4999
4475
  /**
5000
4476
  * POST /bootstrap/:agentId/token
5001
4477
  * GitHub identity → Agent token.
4478
+ * Auto-creates the agent if it does not exist.
5002
4479
  * Only works when the agent has no active tokens.
5003
4480
  */
5004
4481
  app.post("/:agentId/token", async (request, reply) => {
5005
4482
  const { agentId } = request.params;
5006
4483
  const githubUser = request.githubUser;
5007
4484
  if (!githubUser) throw new ForbiddenError("GitHub authentication required");
4485
+ const allowedOrg = app.config.github.allowedOrg;
4486
+ if (!allowedOrg) throw new ForbiddenError("FIRST_TREE_HUB_GITHUB_ALLOWED_ORG is not configured. Agent registration is disabled.");
4487
+ const githubToken = request.headers["x-github-token"];
4488
+ if (!githubToken || typeof githubToken !== "string") throw new ForbiddenError("Missing GitHub token for org membership check");
4489
+ if (!await checkGitHubOrgMembership(githubToken, allowedOrg)) throw new ForbiddenError(`GitHub user "${githubUser.username}" is not a member of organization "${allowedOrg}"`);
5008
4490
  const body = bootstrapTokenRequestSchema.parse(request.body ?? {});
5009
- const result = await bootstrapToken(app.db, agentId, githubUser.username, body.name);
4491
+ const result = await bootstrapToken(app.db, agentId, githubUser.username, {
4492
+ tokenName: body.name,
4493
+ type: body.type,
4494
+ displayName: body.displayName,
4495
+ delegateMention: body.delegateMention,
4496
+ profile: body.profile,
4497
+ metadata: body.metadata
4498
+ });
5010
4499
  return reply.status(201).send({
5011
4500
  id: result.id,
5012
4501
  agentId: result.agentId,
@@ -5018,7 +4507,7 @@ async function bootstrapRoutes(app) {
5018
4507
  });
5019
4508
  /**
5020
4509
  * GET /bootstrap/:agentId/status
5021
- * Check if an agent exists and its status (for polling after PR merge + sync).
4510
+ * Check if an agent exists and its status (for polling).
5022
4511
  */
5023
4512
  app.get("/:agentId/status", async (request) => {
5024
4513
  const { agentId } = request.params;
@@ -5043,11 +4532,9 @@ async function bootstrapRoutes(app) {
5043
4532
  async function contextTreeInfoRoutes(app) {
5044
4533
  /** Public endpoint — returns Context Tree repo metadata for CLI auto-discovery. */
5045
4534
  app.get("/info", async () => {
5046
- const { repo, branch } = app.config.contextTree;
5047
4535
  return {
5048
- repo,
5049
- branch,
5050
- lastSync: null
4536
+ repo: app.config.contextTree?.repo ?? null,
4537
+ branch: app.config.contextTree?.branch ?? null
5051
4538
  };
5052
4539
  });
5053
4540
  }
@@ -6420,6 +5907,7 @@ async function buildApp(config) {
6420
5907
  await api.register(githubWebhookRoutes, { prefix: "/webhooks" });
6421
5908
  await api.register(adminAuthRoutes, { prefix: "/admin/auth" });
6422
5909
  await api.register(contextTreeInfoRoutes, { prefix: "/context-tree" });
5910
+ await api.register(bootstrapConfigRoutes, { prefix: "/bootstrap" });
6423
5911
  await api.register(async (bootstrapApp) => {
6424
5912
  bootstrapApp.addHook("onRequest", githubAuth);
6425
5913
  await bootstrapApp.register(bootstrapRoutes);
@@ -6428,10 +5916,6 @@ async function buildApp(config) {
6428
5916
  adminApp.addHook("onRequest", adminAuth);
6429
5917
  await adminApp.register(adminAgentRoutes);
6430
5918
  }, { 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
5919
  await api.register(async (adminApp) => {
6436
5920
  adminApp.addHook("onRequest", adminAuth);
6437
5921
  await adminApp.register(adminSystemConfigRoutes);
@@ -6467,7 +5951,6 @@ async function buildApp(config) {
6467
5951
  await agentApp.register(agentMessageRoutes, { prefix: "/chats" });
6468
5952
  await agentApp.register(agentSendToAgentRoutes, { prefix: "/agents" });
6469
5953
  await agentApp.register(agentInboxRoutes, { prefix: "/inbox" });
6470
- await agentApp.register(agentContextTreeRoutes, { prefix: "/context-tree" });
6471
5954
  await agentApp.register(agentFeishuBotRoutes);
6472
5955
  await agentApp.register(agentFeishuUserRoutes, { prefix: "/delegated" });
6473
5956
  await agentApp.register(agentWsRoutes(notifier, config.instanceId), { prefix: "/ws" });
@@ -6503,19 +5986,6 @@ async function buildApp(config) {
6503
5986
  backgroundTasks.start();
6504
5987
  await adapterManager.reload();
6505
5988
  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
5989
  });
6520
5990
  app.addHook("onClose", async () => {
6521
5991
  backgroundTasks.stop();
@@ -6635,4 +6105,4 @@ function resolveWebDist() {
6635
6105
  } catch {}
6636
6106
  }
6637
6107
  //#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 };
6108
+ export { createAdminUser$1 as A, printResults as C, isDockerAvailable as D, ensurePostgres as E, cleanWorkspaces as F, FirstTreeHubSDK as M, SdkError as N, stopPostgres as O, SessionRegistry as P, checkWebSocket as S, status as T, checkGitHubToken as _, formatCheckReport as a, checkServerHealth as b, onboardCreate as c, checkAgentConfigs as d, checkAgentTokens as f, checkDocker as g, checkDatabase as h, promptMissingFields as i, hasAdminUser as j, ClientRuntime as k, saveOnboardState as l, checkContextTreeRepo as m, isInteractive as n, loadOnboardState as o, checkClientConfig as p, promptAddAgent as r, onboardCheck as s, startServer as t, runMigrations as u, checkNodeVersion as v, blank as w, checkServerReachable as x, checkServerConfig as y };