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