@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.
- package/dist/{bootstrap-mhkpeOEc.mjs → bootstrap-uyPaaI05.mjs} +14 -12
- package/dist/cli/index.mjs +36 -31
- package/dist/{core-CHL_dgzu.mjs → core-CMeOAZmx.mjs} +267 -797
- package/dist/drizzle/0007_decouple_context_tree.sql +2 -0
- package/dist/drizzle/meta/_journal.json +7 -0
- package/dist/index.mjs +3 -3
- package/dist/web/assets/index-C4J9gHaF.js +280 -0
- package/dist/web/assets/index-vo2Sa6IQ.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BURu6jt9.css +0 -1
- package/dist/web/assets/index-DhpjUi0Y.js +0 -272
|
@@ -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,
|
|
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/
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
*
|
|
846
|
-
*
|
|
847
|
-
*
|
|
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
|
-
}
|
|
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(`##
|
|
865
|
-
}
|
|
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 ? "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2086
|
+
if (args.id) items.push({
|
|
2096
2087
|
key: "id",
|
|
2097
|
-
label: "
|
|
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: "
|
|
2094
|
+
label: "Agent ID",
|
|
2103
2095
|
status: "missing_required",
|
|
2104
|
-
hint: "
|
|
2096
|
+
hint: "Provide via --id"
|
|
2105
2097
|
});
|
|
2106
|
-
|
|
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: "
|
|
2193
|
-
label:
|
|
2194
|
-
status: "
|
|
2195
|
-
|
|
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
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
const
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
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
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
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
|
|
2254
|
-
|
|
2255
|
-
|
|
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
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
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
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
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
|
-
|
|
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 (
|
|
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,
|
|
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 =
|
|
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))}${
|
|
2392
|
-
if (
|
|
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 (
|
|
2395
|
-
|
|
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 ${
|
|
2399
|
-
if (!
|
|
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
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
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
|
-
|
|
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({
|
|
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-
|
|
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
|
-
|
|
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
|
|
3769
|
-
|
|
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
|
-
|
|
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"
|
|
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.
|
|
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
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
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
|
|
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,
|
|
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
|
|
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 {
|
|
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 };
|