@agent-team-foundation/first-tree-hub 0.3.3 → 0.3.5
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-waEJYq3g.mjs → bootstrap-mhkpeOEc.mjs} +14 -6
- package/dist/cli/index.mjs +233 -343
- package/dist/{core-Bl6djdPd.mjs → core-CHL_dgzu.mjs} +304 -263
- package/dist/index.mjs +2 -2
- package/dist/web/assets/{index-BHn3RVzY.js → index-DhpjUi0Y.js} +68 -68
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { a as getGitHubUsername, b as
|
|
2
|
-
import { ZodError, z } from "zod";
|
|
1
|
+
import { S as setConfigValue, a as getGitHubUsername, b as resolveConfigReadonly, c as DEFAULT_CONFIG_DIR, d as agentConfigSchema, f as clientConfigSchema, g as loadAgents, h as initConfig, p as collectMissingPrompts, r as checkBootstrapStatus, s as resolveServerUrl, t as bootstrapToken$1, u as DEFAULT_HOME_DIR$1, x as serverConfigSchema } from "./bootstrap-mhkpeOEc.mjs";
|
|
3
2
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
import {
|
|
4
|
+
import { EventEmitter } from "node:events";
|
|
5
|
+
import WebSocket from "ws";
|
|
6
6
|
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
7
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
8
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
9
|
+
import { ZodError, z } from "zod";
|
|
10
|
+
import "yaml";
|
|
7
11
|
import { homedir } from "node:os";
|
|
8
12
|
import bcrypt from "bcrypt";
|
|
9
13
|
import { and, desc, eq, gt, inArray, isNotNull, isNull, lt, ne, sql } from "drizzle-orm";
|
|
10
14
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
11
15
|
import postgres from "postgres";
|
|
12
|
-
import { execFileSync, execSync } from "node:child_process";
|
|
13
|
-
import { EventEmitter } from "node:events";
|
|
14
|
-
import WebSocket from "ws";
|
|
15
|
-
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
18
18
|
import { input, password, select } from "@inquirer/prompts";
|
|
@@ -24,40 +24,6 @@ import Fastify from "fastify";
|
|
|
24
24
|
import { bigserial, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique } from "drizzle-orm/pg-core";
|
|
25
25
|
import { SignJWT, jwtVerify } from "jose";
|
|
26
26
|
import { Client, EventDispatcher, LoggerLevel, WSClient } from "@larksuiteoapi/node-sdk";
|
|
27
|
-
//#region src/core/admin.ts
|
|
28
|
-
/**
|
|
29
|
-
* Check if any admin user exists.
|
|
30
|
-
*/
|
|
31
|
-
async function hasAdminUser(databaseUrl) {
|
|
32
|
-
const client = postgres(databaseUrl, { max: 1 });
|
|
33
|
-
try {
|
|
34
|
-
return (await client`SELECT count(*)::int AS count FROM admin_users`)[0].count > 0;
|
|
35
|
-
} finally {
|
|
36
|
-
await client.end();
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Create an admin user. Returns the generated password.
|
|
41
|
-
*/
|
|
42
|
-
async function createAdminUser$1(databaseUrl, username, password) {
|
|
43
|
-
const pw = password ?? randomBytes(12).toString("base64url");
|
|
44
|
-
const hash = await bcrypt.hash(pw, 12);
|
|
45
|
-
const client = postgres(databaseUrl, { max: 1 });
|
|
46
|
-
const db = drizzle(client);
|
|
47
|
-
try {
|
|
48
|
-
await db.execute(sql`
|
|
49
|
-
INSERT INTO admin_users (id, username, password_hash, role)
|
|
50
|
-
VALUES (${randomUUID()}, ${username}, ${hash}, 'super_admin')
|
|
51
|
-
`);
|
|
52
|
-
} finally {
|
|
53
|
-
await client.end();
|
|
54
|
-
}
|
|
55
|
-
return {
|
|
56
|
-
username,
|
|
57
|
-
password: pw
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
//#endregion
|
|
61
27
|
//#region ../client/dist/index.mjs
|
|
62
28
|
const FETCH_TIMEOUT_MS = 15e3;
|
|
63
29
|
var FirstTreeHubSDK = class {
|
|
@@ -429,7 +395,7 @@ defineConfig({
|
|
|
429
395
|
"error"
|
|
430
396
|
]).default("info"), { env: "FIRST_TREE_HUB_LOG_LEVEL" })
|
|
431
397
|
});
|
|
432
|
-
const DEFAULT_HOME_DIR = join(homedir(), ".first-tree-hub");
|
|
398
|
+
const DEFAULT_HOME_DIR = process.env.FIRST_TREE_HUB_HOME ?? join(homedir(), ".first-tree-hub");
|
|
433
399
|
join(DEFAULT_HOME_DIR, "config");
|
|
434
400
|
const DEFAULT_DATA_DIR = join(DEFAULT_HOME_DIR, "data");
|
|
435
401
|
defineConfig({
|
|
@@ -494,111 +460,17 @@ defineConfig({
|
|
|
494
460
|
max: field(z.number().default(100), { env: "FIRST_TREE_HUB_RATE_LIMIT_MAX" }),
|
|
495
461
|
loginMax: field(z.number().default(5), { env: "FIRST_TREE_HUB_RATE_LIMIT_LOGIN_MAX" }),
|
|
496
462
|
webhookMax: field(z.number().default(60), { env: "FIRST_TREE_HUB_RATE_LIMIT_WEBHOOK_MAX" })
|
|
463
|
+
}),
|
|
464
|
+
kael: optional({
|
|
465
|
+
endpoint: field(z.string(), { env: "KAEL_ENDPOINT" }),
|
|
466
|
+
apiKey: field(z.string(), {
|
|
467
|
+
env: "KAEL_API_KEY",
|
|
468
|
+
secret: true
|
|
469
|
+
}),
|
|
470
|
+
hubPublicUrl: field(z.string(), { env: "FIRST_TREE_HUB_PUBLIC_URL" })
|
|
497
471
|
})
|
|
498
472
|
});
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Sync the shared Context Tree git clone.
|
|
502
|
-
*
|
|
503
|
-
* Clones on first run, pulls on subsequent runs.
|
|
504
|
-
* Returns the clone path on success, null on failure (graceful degradation).
|
|
505
|
-
*/
|
|
506
|
-
async function syncContextTree(serverUrl, token, log) {
|
|
507
|
-
try {
|
|
508
|
-
execFileSync("git", ["--version"], { stdio: "ignore" });
|
|
509
|
-
} catch {
|
|
510
|
-
log("Context Tree sync skipped: git is not installed");
|
|
511
|
-
return null;
|
|
512
|
-
}
|
|
513
|
-
let repo;
|
|
514
|
-
let branch;
|
|
515
|
-
try {
|
|
516
|
-
const config = await new FirstTreeHubSDK({
|
|
517
|
-
serverUrl,
|
|
518
|
-
token
|
|
519
|
-
}).getContextTreeConfig();
|
|
520
|
-
repo = config.repo;
|
|
521
|
-
branch = config.branch;
|
|
522
|
-
} catch (err) {
|
|
523
|
-
log(`Context Tree sync skipped: failed to fetch config from server (${err instanceof Error ? err.message : String(err)})`);
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
526
|
-
try {
|
|
527
|
-
if (existsSync(join(CONTEXT_TREE_DIR$1, ".git"))) {
|
|
528
|
-
if (execFileSync("git", [
|
|
529
|
-
"rev-parse",
|
|
530
|
-
"--abbrev-ref",
|
|
531
|
-
"HEAD"
|
|
532
|
-
], {
|
|
533
|
-
cwd: CONTEXT_TREE_DIR$1,
|
|
534
|
-
encoding: "utf-8",
|
|
535
|
-
timeout: 5e3
|
|
536
|
-
}).trim() !== branch) {
|
|
537
|
-
execFileSync("git", ["checkout", branch], {
|
|
538
|
-
cwd: CONTEXT_TREE_DIR$1,
|
|
539
|
-
stdio: "pipe",
|
|
540
|
-
timeout: 1e4
|
|
541
|
-
});
|
|
542
|
-
log(`Context Tree switched to branch ${branch}`);
|
|
543
|
-
}
|
|
544
|
-
execFileSync("git", ["pull", "--ff-only"], {
|
|
545
|
-
cwd: CONTEXT_TREE_DIR$1,
|
|
546
|
-
stdio: "pipe",
|
|
547
|
-
timeout: 3e4
|
|
548
|
-
});
|
|
549
|
-
log(`Context Tree updated (pull)`);
|
|
550
|
-
} else {
|
|
551
|
-
mkdirSync(CONTEXT_TREE_DIR$1, { recursive: true });
|
|
552
|
-
execFileSync("git", [
|
|
553
|
-
"clone",
|
|
554
|
-
"--branch",
|
|
555
|
-
branch,
|
|
556
|
-
"--single-branch",
|
|
557
|
-
repo,
|
|
558
|
-
CONTEXT_TREE_DIR$1
|
|
559
|
-
], {
|
|
560
|
-
stdio: "pipe",
|
|
561
|
-
timeout: 6e4
|
|
562
|
-
});
|
|
563
|
-
log(`Context Tree cloned from ${repo} (branch: ${branch})`);
|
|
564
|
-
}
|
|
565
|
-
return CONTEXT_TREE_DIR$1;
|
|
566
|
-
} catch (err) {
|
|
567
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
568
|
-
log(`Context Tree sync failed: ${msg}`);
|
|
569
|
-
log("Check that git credentials (SSH key or credential helper) are configured for this repo");
|
|
570
|
-
if ((msg.includes("cannot fast-forward") || msg.includes("not possible to fast-forward") || msg.includes("CONFLICT")) && existsSync(join(CONTEXT_TREE_DIR$1, ".git"))) {
|
|
571
|
-
log("Diverged history detected, attempting fresh clone...");
|
|
572
|
-
try {
|
|
573
|
-
rmSync(CONTEXT_TREE_DIR$1, {
|
|
574
|
-
recursive: true,
|
|
575
|
-
force: true
|
|
576
|
-
});
|
|
577
|
-
mkdirSync(CONTEXT_TREE_DIR$1, { recursive: true });
|
|
578
|
-
execFileSync("git", [
|
|
579
|
-
"clone",
|
|
580
|
-
"--branch",
|
|
581
|
-
branch,
|
|
582
|
-
"--single-branch",
|
|
583
|
-
repo,
|
|
584
|
-
CONTEXT_TREE_DIR$1
|
|
585
|
-
], {
|
|
586
|
-
stdio: "pipe",
|
|
587
|
-
timeout: 6e4
|
|
588
|
-
});
|
|
589
|
-
log("Context Tree re-cloned successfully");
|
|
590
|
-
return CONTEXT_TREE_DIR$1;
|
|
591
|
-
} catch {
|
|
592
|
-
log("Context Tree re-clone also failed, continuing without context");
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
if (existsSync(join(CONTEXT_TREE_DIR$1, ".git"))) {
|
|
596
|
-
log("Using existing Context Tree clone despite sync failure");
|
|
597
|
-
return CONTEXT_TREE_DIR$1;
|
|
598
|
-
}
|
|
599
|
-
return null;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
473
|
+
join(DEFAULT_DATA_DIR, "context-tree");
|
|
602
474
|
/**
|
|
603
475
|
* Bootstrap a workspace with .agent/ directory files.
|
|
604
476
|
*
|
|
@@ -1454,92 +1326,44 @@ const agentSlotConfigSchema = z.object({
|
|
|
1454
1326
|
session: sessionConfigSchema.prefault({}),
|
|
1455
1327
|
concurrency: z.number().int().positive().default(5)
|
|
1456
1328
|
});
|
|
1457
|
-
|
|
1329
|
+
z.object({
|
|
1458
1330
|
server: z.url().default("http://localhost:8000"),
|
|
1459
1331
|
agents: z.record(z.string(), agentSlotConfigSchema).refine((agents) => Object.keys(agents).length > 0, "At least one agent must be defined")
|
|
1460
1332
|
});
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
const result = {};
|
|
1473
|
-
for (const [key, value] of Object.entries(obj)) result[key] = deepExpandEnv(value);
|
|
1474
|
-
return result;
|
|
1333
|
+
//#endregion
|
|
1334
|
+
//#region src/core/admin.ts
|
|
1335
|
+
/**
|
|
1336
|
+
* Check if any admin user exists.
|
|
1337
|
+
*/
|
|
1338
|
+
async function hasAdminUser(databaseUrl) {
|
|
1339
|
+
const client = postgres(databaseUrl, { max: 1 });
|
|
1340
|
+
try {
|
|
1341
|
+
return (await client`SELECT count(*)::int AS count FROM admin_users`)[0].count > 0;
|
|
1342
|
+
} finally {
|
|
1343
|
+
await client.end();
|
|
1475
1344
|
}
|
|
1476
|
-
return obj;
|
|
1477
1345
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
const
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
this.slots.push(new AgentSlot({
|
|
1494
|
-
name,
|
|
1495
|
-
serverUrl: this.config.server,
|
|
1496
|
-
token: agentConfig.token,
|
|
1497
|
-
type: agentConfig.type,
|
|
1498
|
-
handlerFactory,
|
|
1499
|
-
session: agentConfig.session,
|
|
1500
|
-
concurrency: agentConfig.concurrency
|
|
1501
|
-
}));
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
/** Start all agent slots and block until shutdown signal. */
|
|
1505
|
-
async start() {
|
|
1506
|
-
const log = (msg) => process.stderr.write(`[runtime] ${msg}\n`);
|
|
1507
|
-
const firstToken = Object.values(this.config.agents)[0]?.token;
|
|
1508
|
-
let contextTreePath = null;
|
|
1509
|
-
if (firstToken) contextTreePath = await syncContextTree(this.config.server, firstToken, log);
|
|
1510
|
-
if (!contextTreePath) log("WARNING: Context Tree sync failed — agents will start without organizational context");
|
|
1511
|
-
log(`Starting ${this.slots.length} agent(s)...`);
|
|
1512
|
-
const results = await Promise.allSettled(this.slots.map((slot) => slot.start(contextTreePath)));
|
|
1513
|
-
let failed = 0;
|
|
1514
|
-
for (const result of results) if (result.status === "rejected") {
|
|
1515
|
-
log(`Failed to start agent: ${result.reason instanceof Error ? result.reason.message : result.reason}`);
|
|
1516
|
-
failed++;
|
|
1517
|
-
}
|
|
1518
|
-
if (failed === this.slots.length) throw new Error("All agents failed to start");
|
|
1519
|
-
log("Ready. Press Ctrl+C to stop.");
|
|
1520
|
-
await new Promise((resolve) => {
|
|
1521
|
-
const shutdown = async () => {
|
|
1522
|
-
if (this.stopping) return;
|
|
1523
|
-
this.stopping = true;
|
|
1524
|
-
log("Shutting down...");
|
|
1525
|
-
const timer = setTimeout(() => {
|
|
1526
|
-
log("Shutdown timeout reached, forcing exit");
|
|
1527
|
-
process.exit(1);
|
|
1528
|
-
}, this.shutdownTimeout);
|
|
1529
|
-
await this.stop();
|
|
1530
|
-
clearTimeout(timer);
|
|
1531
|
-
log("Stopped");
|
|
1532
|
-
resolve();
|
|
1533
|
-
};
|
|
1534
|
-
process.on("SIGINT", shutdown);
|
|
1535
|
-
process.on("SIGTERM", shutdown);
|
|
1536
|
-
});
|
|
1537
|
-
}
|
|
1538
|
-
/** Stop all slots. */
|
|
1539
|
-
async stop() {
|
|
1540
|
-
await Promise.allSettled(this.slots.map((slot) => slot.stop()));
|
|
1346
|
+
/**
|
|
1347
|
+
* Create an admin user. Returns the generated password.
|
|
1348
|
+
*/
|
|
1349
|
+
async function createAdminUser$1(databaseUrl, username, password) {
|
|
1350
|
+
const pw = password ?? randomBytes(12).toString("base64url");
|
|
1351
|
+
const hash = await bcrypt.hash(pw, 12);
|
|
1352
|
+
const client = postgres(databaseUrl, { max: 1 });
|
|
1353
|
+
const db = drizzle(client);
|
|
1354
|
+
try {
|
|
1355
|
+
await db.execute(sql`
|
|
1356
|
+
INSERT INTO admin_users (id, username, password_hash, role)
|
|
1357
|
+
VALUES (${randomUUID()}, ${username}, ${hash}, 'super_admin')
|
|
1358
|
+
`);
|
|
1359
|
+
} finally {
|
|
1360
|
+
await client.end();
|
|
1541
1361
|
}
|
|
1542
|
-
|
|
1362
|
+
return {
|
|
1363
|
+
username,
|
|
1364
|
+
password: pw
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1543
1367
|
//#endregion
|
|
1544
1368
|
//#region src/core/client-runtime.ts
|
|
1545
1369
|
/**
|
|
@@ -2187,10 +2011,10 @@ async function runMigrations(databaseUrl) {
|
|
|
2187
2011
|
}
|
|
2188
2012
|
//#endregion
|
|
2189
2013
|
//#region src/core/onboard.ts
|
|
2190
|
-
const STATE_FILE = join(
|
|
2014
|
+
const STATE_FILE = join(DEFAULT_HOME_DIR$1, ".onboard-state.json");
|
|
2191
2015
|
/** Save current onboard args to state file for resume. */
|
|
2192
2016
|
function saveOnboardState(args) {
|
|
2193
|
-
mkdirSync(
|
|
2017
|
+
mkdirSync(DEFAULT_HOME_DIR$1, { recursive: true });
|
|
2194
2018
|
writeFileSync(STATE_FILE, JSON.stringify({ args }, null, 2));
|
|
2195
2019
|
}
|
|
2196
2020
|
/** Load saved onboard args from state file. */
|
|
@@ -2323,7 +2147,7 @@ async function onboardCheck(args) {
|
|
|
2323
2147
|
status: "missing_optional",
|
|
2324
2148
|
hint: `defaults to "${args.id ?? ""}"`
|
|
2325
2149
|
});
|
|
2326
|
-
items.push(args.assistant ? {
|
|
2150
|
+
if (args.type === "human") items.push(args.assistant ? {
|
|
2327
2151
|
key: "assistant",
|
|
2328
2152
|
label: "assistant",
|
|
2329
2153
|
status: "ok",
|
|
@@ -2334,7 +2158,7 @@ async function onboardCheck(args) {
|
|
|
2334
2158
|
status: "missing_optional",
|
|
2335
2159
|
hint: "Also create a personal_assistant"
|
|
2336
2160
|
});
|
|
2337
|
-
items.push(args.feishuBotAppId ? {
|
|
2161
|
+
if (args.type !== "human" || args.assistant) items.push(args.feishuBotAppId ? {
|
|
2338
2162
|
key: "feishu_bot",
|
|
2339
2163
|
label: "feishu-bot-app-id",
|
|
2340
2164
|
status: "ok",
|
|
@@ -2343,7 +2167,7 @@ async function onboardCheck(args) {
|
|
|
2343
2167
|
key: "feishu_bot",
|
|
2344
2168
|
label: "feishu-bot-app-id",
|
|
2345
2169
|
status: "missing_optional",
|
|
2346
|
-
hint: "Feishu bot App ID
|
|
2170
|
+
hint: "Feishu bot App ID"
|
|
2347
2171
|
});
|
|
2348
2172
|
if (args.id && repoPath) if (existsSync(join(repoPath, "members", args.id))) try {
|
|
2349
2173
|
execSync(`git ls-files --error-unmatch members/${args.id}/NODE.md`, {
|
|
@@ -2385,6 +2209,7 @@ function formatCheckReport(items) {
|
|
|
2385
2209
|
async function onboardCreate(args) {
|
|
2386
2210
|
const repoPath = await resolveContextTreeRepo(args.server);
|
|
2387
2211
|
if (!repoPath) throw new Error("Context Tree repo not available. Ensure --server is configured and the server is running.");
|
|
2212
|
+
if (args.assistant && args.type !== "human") throw new Error(`--assistant is only valid for human agents, not ${args.type}`);
|
|
2388
2213
|
const ghUsername = getGitHubUsername();
|
|
2389
2214
|
const githubField = args.type === "human" ? ghUsername : null;
|
|
2390
2215
|
const humanNodePath = join(repoPath, "members", args.id, "NODE.md");
|
|
@@ -2507,7 +2332,7 @@ async function onboardCreate(args) {
|
|
|
2507
2332
|
branch,
|
|
2508
2333
|
prUrl: prOutput
|
|
2509
2334
|
};
|
|
2510
|
-
mkdirSync(
|
|
2335
|
+
mkdirSync(DEFAULT_HOME_DIR$1, { recursive: true });
|
|
2511
2336
|
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
2512
2337
|
return { prUrl: prOutput };
|
|
2513
2338
|
}
|
|
@@ -2550,7 +2375,7 @@ async function onboardContinue(args) {
|
|
|
2550
2375
|
first-tree-hub onboard --continue`);
|
|
2551
2376
|
throw err;
|
|
2552
2377
|
}
|
|
2553
|
-
process.stderr.write(`Token saved to
|
|
2378
|
+
process.stderr.write(`Token saved to ${DEFAULT_HOME_DIR$1}/config/agents/${agentToBootstrap}/agent.yaml\n`);
|
|
2554
2379
|
if (mergedArgs.feishuBotAppId && mergedArgs.feishuBotAppSecret) {
|
|
2555
2380
|
const { bindFeishuBot } = await import("./feishu-Y4m2zFc3.mjs").then((n) => n.r);
|
|
2556
2381
|
process.stderr.write("Binding Feishu bot...\n");
|
|
@@ -2561,16 +2386,20 @@ async function onboardContinue(args) {
|
|
|
2561
2386
|
const { unlinkSync } = await import("node:fs");
|
|
2562
2387
|
unlinkSync(STATE_FILE);
|
|
2563
2388
|
} catch {}
|
|
2389
|
+
const typeLabel = mergedArgs.type === "human" ? "Human" : mergedArgs.type === "autonomous_agent" ? "Agent" : "Assistant";
|
|
2564
2390
|
process.stderr.write("\n✅ Onboard complete!\n\n");
|
|
2565
|
-
process.stderr.write(`
|
|
2391
|
+
process.stderr.write(` ${typeLabel}:${" ".repeat(Math.max(1, 10 - typeLabel.length))}${mergedArgs.id}\n`);
|
|
2566
2392
|
if (mergedArgs.assistant) process.stderr.write(` Assistant: ${mergedArgs.assistant}\n`);
|
|
2567
|
-
process.stderr.write(` Token:
|
|
2393
|
+
process.stderr.write(` Token: ${DEFAULT_HOME_DIR$1}/config/agents/${agentToBootstrap}/agent.yaml\n`);
|
|
2568
2394
|
if (mergedArgs.feishuBotAppId) process.stderr.write(` Feishu: bot bound (${mergedArgs.feishuBotAppId})\n`);
|
|
2395
|
+
setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", serverUrl);
|
|
2569
2396
|
if (mergedArgs.type === "human") {
|
|
2570
2397
|
process.stderr.write("\n Next step — bind your Feishu account:\n");
|
|
2571
2398
|
process.stderr.write(` Send this message to the bot in Feishu: /bind ${mergedArgs.id}\n`);
|
|
2572
2399
|
if (!mergedArgs.feishuBotAppId) process.stderr.write(" (requires a Feishu bot to be configured in the system)\n");
|
|
2573
2400
|
}
|
|
2401
|
+
process.stderr.write("\n Start the agent:\n");
|
|
2402
|
+
process.stderr.write(" first-tree-hub client start\n");
|
|
2574
2403
|
process.stderr.write("\n");
|
|
2575
2404
|
}
|
|
2576
2405
|
function createMemberNodeMd(repoPath, data) {
|
|
@@ -2578,7 +2407,16 @@ function createMemberNodeMd(repoPath, data) {
|
|
|
2578
2407
|
mkdirSync(memberDir, { recursive: true });
|
|
2579
2408
|
const domainsList = data.domains.map((d) => ` - "${d}"`).join("\n");
|
|
2580
2409
|
const githubLine = data.github ? `\ngithub: ${data.github}` : "";
|
|
2581
|
-
const delegateLine = data.delegateMention ? `\ndelegate_mention: ${data.delegateMention}` : "";
|
|
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
|
+
`;
|
|
2582
2420
|
const content = `---
|
|
2583
2421
|
title: "${data.displayName}"
|
|
2584
2422
|
owners: [${data.owner}]
|
|
@@ -2590,10 +2428,7 @@ ${domainsList}${githubLine}${delegateLine}
|
|
|
2590
2428
|
|
|
2591
2429
|
# ${data.displayName}
|
|
2592
2430
|
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
## Current Focus
|
|
2596
|
-
`;
|
|
2431
|
+
${bodySections}`;
|
|
2597
2432
|
writeFileSync(join(memberDir, "NODE.md"), content);
|
|
2598
2433
|
}
|
|
2599
2434
|
function isTrackedByGit(repoPath, filePath) {
|
|
@@ -2607,9 +2442,9 @@ function isTrackedByGit(repoPath, filePath) {
|
|
|
2607
2442
|
return false;
|
|
2608
2443
|
}
|
|
2609
2444
|
}
|
|
2610
|
-
const CONTEXT_TREE_DIR = join(
|
|
2445
|
+
const CONTEXT_TREE_DIR = join(DEFAULT_HOME_DIR$1, "context-tree");
|
|
2611
2446
|
/**
|
|
2612
|
-
* Resolve Context Tree to a **local path** at
|
|
2447
|
+
* Resolve Context Tree to a **local path** at $FIRST_TREE_HUB_HOME/context-tree/.
|
|
2613
2448
|
*
|
|
2614
2449
|
* Repo URL is obtained from the Hub server. The local clone is always
|
|
2615
2450
|
* managed in the standard location — no custom paths allowed.
|
|
@@ -2656,13 +2491,13 @@ async function resolveContextTreeRepo(serverUrl) {
|
|
|
2656
2491
|
return CONTEXT_TREE_DIR;
|
|
2657
2492
|
}
|
|
2658
2493
|
} catch {}
|
|
2659
|
-
const safePrefix =
|
|
2494
|
+
const safePrefix = DEFAULT_HOME_DIR$1;
|
|
2660
2495
|
if (!CONTEXT_TREE_DIR.startsWith(safePrefix) || CONTEXT_TREE_DIR === safePrefix) throw new Error(`Refusing to delete unsafe path: ${CONTEXT_TREE_DIR}`);
|
|
2661
2496
|
execSync(`rm -rf ${CONTEXT_TREE_DIR}`);
|
|
2662
2497
|
}
|
|
2663
2498
|
try {
|
|
2664
2499
|
process.stderr.write(`Cloning Context Tree to ${CONTEXT_TREE_DIR}...\n`);
|
|
2665
|
-
mkdirSync(
|
|
2500
|
+
mkdirSync(DEFAULT_HOME_DIR$1, { recursive: true });
|
|
2666
2501
|
execSync(`git ${gitConfigArgs} clone ${repoUrl} ${CONTEXT_TREE_DIR}`, {
|
|
2667
2502
|
stdio: "pipe",
|
|
2668
2503
|
env: gitEnv
|
|
@@ -2727,8 +2562,7 @@ async function promptMissingFields(options) {
|
|
|
2727
2562
|
const envStr = envHint ? ` (env: ${envHint})` : "";
|
|
2728
2563
|
return ` ${m.dotPath}${envStr}`;
|
|
2729
2564
|
});
|
|
2730
|
-
throw new Error(`Missing required configuration:\n${lines.join("\n")}\n\nProvide values via environment variables, config file (
|
|
2731
|
-
or run without --no-interactive to use the interactive setup wizard.`);
|
|
2565
|
+
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.`);
|
|
2732
2566
|
}
|
|
2733
2567
|
const configPath = join(options.configDir ?? DEFAULT_CONFIG_DIR, `${options.role}.yaml`);
|
|
2734
2568
|
const results = {};
|
|
@@ -2808,7 +2642,11 @@ function setNestedByDot(obj, dotPath, value) {
|
|
|
2808
2642
|
}
|
|
2809
2643
|
//#endregion
|
|
2810
2644
|
//#region ../shared/dist/index.mjs
|
|
2811
|
-
const adapterPlatformSchema = z.enum([
|
|
2645
|
+
const adapterPlatformSchema = z.enum([
|
|
2646
|
+
"feishu",
|
|
2647
|
+
"slack",
|
|
2648
|
+
"kael"
|
|
2649
|
+
]);
|
|
2812
2650
|
const adapterStatusSchema = z.enum(["active", "inactive"]);
|
|
2813
2651
|
const createAdapterConfigSchema = z.object({
|
|
2814
2652
|
platform: adapterPlatformSchema,
|
|
@@ -3081,7 +2919,7 @@ const SYSTEM_CONFIG_DEFAULTS = {
|
|
|
3081
2919
|
[SYSTEM_CONFIG_KEYS.PRESENCE_CLEANUP_SECONDS]: 60
|
|
3082
2920
|
};
|
|
3083
2921
|
//#endregion
|
|
3084
|
-
//#region ../server/dist/app-
|
|
2922
|
+
//#region ../server/dist/app-CurdzcN2.mjs
|
|
3085
2923
|
var __defProp = Object.defineProperty;
|
|
3086
2924
|
var __exportAll = (all, no_symbols) => {
|
|
3087
2925
|
let target = {};
|
|
@@ -5013,7 +4851,7 @@ async function pollInbox(db, inboxId, limit) {
|
|
|
5013
4851
|
});
|
|
5014
4852
|
});
|
|
5015
4853
|
}
|
|
5016
|
-
async function ackEntry$
|
|
4854
|
+
async function ackEntry$2(db, entryId, inboxId) {
|
|
5017
4855
|
const [entry] = await db.update(inboxEntries).set({
|
|
5018
4856
|
status: "acked",
|
|
5019
4857
|
ackedAt: /* @__PURE__ */ new Date()
|
|
@@ -5055,7 +4893,7 @@ async function agentInboxRoutes(app) {
|
|
|
5055
4893
|
app.post("/:entryId/ack", async (request, reply) => {
|
|
5056
4894
|
const identity = requireAgent(request);
|
|
5057
4895
|
const entryId = Number(request.params.entryId);
|
|
5058
|
-
await ackEntry$
|
|
4896
|
+
await ackEntry$2(app.db, entryId, identity.inboxId);
|
|
5059
4897
|
return reply.status(204).send();
|
|
5060
4898
|
});
|
|
5061
4899
|
app.post("/:entryId/renew", async (request, reply) => {
|
|
@@ -5828,7 +5666,7 @@ async function withoutProxy(fn) {
|
|
|
5828
5666
|
for (const [key, val] of Object.entries(saved)) process.env[key] = val;
|
|
5829
5667
|
}
|
|
5830
5668
|
}
|
|
5831
|
-
const OUTBOUND_BATCH_SIZE = 10;
|
|
5669
|
+
const OUTBOUND_BATCH_SIZE$1 = 10;
|
|
5832
5670
|
/** Wrap an SDK API call with proxy bypass if needed. */
|
|
5833
5671
|
function botApiCall(bot, fn) {
|
|
5834
5672
|
return bot.bypassProxy ? withoutProxy(fn) : fn();
|
|
@@ -6003,6 +5841,8 @@ function parseEventData(appId, data) {
|
|
|
6003
5841
|
const sender = data.sender;
|
|
6004
5842
|
const message = data.message;
|
|
6005
5843
|
if (!sender?.sender_id?.open_id || !message) return null;
|
|
5844
|
+
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`);
|
|
5845
|
+
const resolvedSenderId = sender.sender_id.union_id ?? sender.sender_id.open_id;
|
|
6006
5846
|
const eventId = data.event_id ?? `ws_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
6007
5847
|
let parsedContent;
|
|
6008
5848
|
try {
|
|
@@ -6019,7 +5859,8 @@ function parseEventData(appId, data) {
|
|
|
6019
5859
|
eventId,
|
|
6020
5860
|
platform: "feishu",
|
|
6021
5861
|
appId,
|
|
6022
|
-
senderId:
|
|
5862
|
+
senderId: resolvedSenderId,
|
|
5863
|
+
senderOpenId: sender.sender_id.open_id,
|
|
6023
5864
|
senderType: sender.sender_type ?? "user",
|
|
6024
5865
|
externalChannelId: message.chat_id ?? "",
|
|
6025
5866
|
chatType: message.chat_type ?? "group",
|
|
@@ -6192,7 +6033,7 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
|
|
|
6192
6033
|
JOIN adapter_agent_mappings aam ON a.id = aam.agent_id
|
|
6193
6034
|
WHERE aam.platform = 'feishu' AND ie.status = 'pending'
|
|
6194
6035
|
ORDER BY ie.created_at
|
|
6195
|
-
LIMIT ${OUTBOUND_BATCH_SIZE}
|
|
6036
|
+
LIMIT ${OUTBOUND_BATCH_SIZE$1}
|
|
6196
6037
|
FOR UPDATE SKIP LOCKED
|
|
6197
6038
|
)
|
|
6198
6039
|
RETURNING id, inbox_id, message_id, chat_id
|
|
@@ -6201,21 +6042,21 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
|
|
|
6201
6042
|
for (const entry of claimed) try {
|
|
6202
6043
|
const [msg] = await db.select().from(messages).where(eq(messages.id, entry.message_id)).limit(1);
|
|
6203
6044
|
if (!msg) {
|
|
6204
|
-
await ackEntry(db, entry.id);
|
|
6045
|
+
await ackEntry$1(db, entry.id);
|
|
6205
6046
|
continue;
|
|
6206
6047
|
}
|
|
6207
6048
|
if (msg.metadata?.source === "feishu") {
|
|
6208
|
-
await ackEntry(db, entry.id);
|
|
6049
|
+
await ackEntry$1(db, entry.id);
|
|
6209
6050
|
continue;
|
|
6210
6051
|
}
|
|
6211
6052
|
const channelMapping = await findExternalChannelByChat(db, "feishu", entry.chat_id ?? msg.chatId);
|
|
6212
6053
|
if (!channelMapping) {
|
|
6213
|
-
await ackEntry(db, entry.id);
|
|
6054
|
+
await ackEntry$1(db, entry.id);
|
|
6214
6055
|
continue;
|
|
6215
6056
|
}
|
|
6216
6057
|
const dedupKey = `${msg.id}:${channelMapping.externalChannelId}`;
|
|
6217
6058
|
if (sentMessages.has(dedupKey)) {
|
|
6218
|
-
await ackEntry(db, entry.id);
|
|
6059
|
+
await ackEntry$1(db, entry.id);
|
|
6219
6060
|
continue;
|
|
6220
6061
|
}
|
|
6221
6062
|
const bot = findBotByAgentId(msg.senderId);
|
|
@@ -6224,7 +6065,7 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
|
|
|
6224
6065
|
messageId: msg.id,
|
|
6225
6066
|
senderId: msg.senderId
|
|
6226
6067
|
}, "Outbound skip: sender has no feishu bot binding");
|
|
6227
|
-
await ackEntry(db, entry.id);
|
|
6068
|
+
await ackEntry$1(db, entry.id);
|
|
6228
6069
|
continue;
|
|
6229
6070
|
}
|
|
6230
6071
|
const { msgType, content } = formatForFeishu(msg.format, msg.content);
|
|
@@ -6244,7 +6085,7 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
|
|
|
6244
6085
|
externalMessageId: externalMsgId,
|
|
6245
6086
|
externalChannelId: channelMapping.externalChannelId
|
|
6246
6087
|
});
|
|
6247
|
-
await ackEntry(db, entry.id);
|
|
6088
|
+
await ackEntry$1(db, entry.id);
|
|
6248
6089
|
bot.lastActiveAt = /* @__PURE__ */ new Date();
|
|
6249
6090
|
sent++;
|
|
6250
6091
|
} catch (err) {
|
|
@@ -6259,7 +6100,7 @@ async function processFeishuOutbound(db, findBotByAgentId, log) {
|
|
|
6259
6100
|
errors: errorCount
|
|
6260
6101
|
};
|
|
6261
6102
|
}
|
|
6262
|
-
async function ackEntry(db, entryId) {
|
|
6103
|
+
async function ackEntry$1(db, entryId) {
|
|
6263
6104
|
await db.update(inboxEntries).set({
|
|
6264
6105
|
status: "acked",
|
|
6265
6106
|
ackedAt: /* @__PURE__ */ new Date()
|
|
@@ -6297,10 +6138,11 @@ function formatForFeishu(format, content) {
|
|
|
6297
6138
|
content: JSON.stringify({ text })
|
|
6298
6139
|
};
|
|
6299
6140
|
}
|
|
6300
|
-
function createBackgroundTasks(app, instanceId, adapterManager) {
|
|
6141
|
+
function createBackgroundTasks(app, instanceId, adapterManager, kaelRuntime) {
|
|
6301
6142
|
let inboxTimer = null;
|
|
6302
6143
|
let heartbeatTimer = null;
|
|
6303
6144
|
let adapterOutboundTimer = null;
|
|
6145
|
+
let kaelOutboundTimer = null;
|
|
6304
6146
|
return {
|
|
6305
6147
|
start() {
|
|
6306
6148
|
inboxTimer = setInterval(async () => {
|
|
@@ -6329,6 +6171,13 @@ function createBackgroundTasks(app, instanceId, adapterManager) {
|
|
|
6329
6171
|
app.log.error(err, "Adapter outbound processing failed");
|
|
6330
6172
|
}
|
|
6331
6173
|
}, 5e3);
|
|
6174
|
+
if (kaelRuntime) kaelOutboundTimer = setInterval(async () => {
|
|
6175
|
+
try {
|
|
6176
|
+
await kaelRuntime.processOutbound();
|
|
6177
|
+
} catch (err) {
|
|
6178
|
+
app.log.error(err, "Kael outbound processing failed");
|
|
6179
|
+
}
|
|
6180
|
+
}, 5e3);
|
|
6332
6181
|
heartbeatInstance(app.db, instanceId).catch((err) => {
|
|
6333
6182
|
app.log.error(err, "Failed initial heartbeat");
|
|
6334
6183
|
});
|
|
@@ -6346,9 +6195,195 @@ function createBackgroundTasks(app, instanceId, adapterManager) {
|
|
|
6346
6195
|
clearInterval(adapterOutboundTimer);
|
|
6347
6196
|
adapterOutboundTimer = null;
|
|
6348
6197
|
}
|
|
6198
|
+
if (kaelOutboundTimer) {
|
|
6199
|
+
clearInterval(kaelOutboundTimer);
|
|
6200
|
+
kaelOutboundTimer = null;
|
|
6201
|
+
}
|
|
6202
|
+
}
|
|
6203
|
+
};
|
|
6204
|
+
}
|
|
6205
|
+
const OUTBOUND_BATCH_SIZE = 10;
|
|
6206
|
+
function createKaelRuntime(db, encryptionKey, kaelEndpoint, kaelApiKey, serverUrl, log) {
|
|
6207
|
+
const agentConfigs = /* @__PURE__ */ new Map();
|
|
6208
|
+
const inboxToConfig = /* @__PURE__ */ new Map();
|
|
6209
|
+
let aborted = false;
|
|
6210
|
+
return {
|
|
6211
|
+
async reload() {
|
|
6212
|
+
if (!encryptionKey) {
|
|
6213
|
+
log.warn("Encryption key not set — Kael runtime disabled");
|
|
6214
|
+
return;
|
|
6215
|
+
}
|
|
6216
|
+
if (!kaelEndpoint) {
|
|
6217
|
+
log.debug("KAEL_ENDPOINT not configured — Kael runtime idle");
|
|
6218
|
+
agentConfigs.clear();
|
|
6219
|
+
inboxToConfig.clear();
|
|
6220
|
+
return;
|
|
6221
|
+
}
|
|
6222
|
+
const configs = await db.select().from(adapterConfigs).where(and(eq(adapterConfigs.platform, "kael"), eq(adapterConfigs.status, "active")));
|
|
6223
|
+
const configAgentIds = configs.filter((c) => c.credentials).map((c) => c.agentId);
|
|
6224
|
+
const agentRows = configAgentIds.length > 0 ? await db.execute(sql`SELECT id, inbox_id FROM agents WHERE id IN (${sql.join(configAgentIds.map((id) => sql`${id}`), sql`, `)}) AND status = 'active'`) : [];
|
|
6225
|
+
const agentInboxMap = new Map(agentRows.map((a) => [a.id, a.inbox_id]));
|
|
6226
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6227
|
+
for (const config of configs) {
|
|
6228
|
+
if (!config.credentials) continue;
|
|
6229
|
+
let creds;
|
|
6230
|
+
try {
|
|
6231
|
+
creds = decryptCredentials(config.credentials, encryptionKey);
|
|
6232
|
+
} catch (err) {
|
|
6233
|
+
log.error({
|
|
6234
|
+
configId: config.id,
|
|
6235
|
+
err
|
|
6236
|
+
}, "Failed to decrypt Kael adapter credentials");
|
|
6237
|
+
continue;
|
|
6238
|
+
}
|
|
6239
|
+
seen.add(config.agentId);
|
|
6240
|
+
const inboxId = agentInboxMap.get(config.agentId);
|
|
6241
|
+
if (!inboxId) {
|
|
6242
|
+
log.warn({
|
|
6243
|
+
configId: config.id,
|
|
6244
|
+
agentId: config.agentId
|
|
6245
|
+
}, "Kael config agent not found or inactive");
|
|
6246
|
+
continue;
|
|
6247
|
+
}
|
|
6248
|
+
const entry = {
|
|
6249
|
+
configId: config.id,
|
|
6250
|
+
agentId: config.agentId,
|
|
6251
|
+
inboxId,
|
|
6252
|
+
kaelUserId: creds.kaelUserId,
|
|
6253
|
+
kaelProjectId: creds.kaelProjectId,
|
|
6254
|
+
agentToken: creds.agentToken
|
|
6255
|
+
};
|
|
6256
|
+
agentConfigs.set(config.agentId, entry);
|
|
6257
|
+
inboxToConfig.set(inboxId, entry);
|
|
6258
|
+
log.info({
|
|
6259
|
+
configId: config.id,
|
|
6260
|
+
agentId: config.agentId
|
|
6261
|
+
}, "Loaded Kael adapter config");
|
|
6262
|
+
}
|
|
6263
|
+
for (const agentId of agentConfigs.keys()) if (!seen.has(agentId)) {
|
|
6264
|
+
const old = agentConfigs.get(agentId);
|
|
6265
|
+
if (old) inboxToConfig.delete(old.inboxId);
|
|
6266
|
+
agentConfigs.delete(agentId);
|
|
6267
|
+
log.info({ agentId }, "Removed inactive Kael adapter config");
|
|
6268
|
+
}
|
|
6269
|
+
},
|
|
6270
|
+
async processOutbound() {
|
|
6271
|
+
if (agentConfigs.size === 0 || !kaelEndpoint || aborted) return {
|
|
6272
|
+
sent: 0,
|
|
6273
|
+
errors: 0
|
|
6274
|
+
};
|
|
6275
|
+
let sent = 0;
|
|
6276
|
+
let errorCount = 0;
|
|
6277
|
+
try {
|
|
6278
|
+
const agentIds = [...agentConfigs.keys()];
|
|
6279
|
+
const claimed = await db.execute(sql`
|
|
6280
|
+
UPDATE inbox_entries
|
|
6281
|
+
SET status = 'delivered', delivered_at = NOW()
|
|
6282
|
+
WHERE id IN (
|
|
6283
|
+
SELECT ie.id FROM inbox_entries ie
|
|
6284
|
+
JOIN agents a ON ie.inbox_id = a.inbox_id
|
|
6285
|
+
JOIN adapter_configs ac ON a.id = ac.agent_id
|
|
6286
|
+
WHERE ac.platform = 'kael' AND ac.status = 'active'
|
|
6287
|
+
AND ie.status = 'pending'
|
|
6288
|
+
AND a.id IN (${sql.join(agentIds.map((id) => sql`${id}`), sql`, `)})
|
|
6289
|
+
ORDER BY ie.created_at
|
|
6290
|
+
LIMIT ${OUTBOUND_BATCH_SIZE}
|
|
6291
|
+
FOR UPDATE OF ie SKIP LOCKED
|
|
6292
|
+
)
|
|
6293
|
+
RETURNING id, inbox_id, message_id, chat_id
|
|
6294
|
+
`);
|
|
6295
|
+
for (const entry of claimed) try {
|
|
6296
|
+
const [msg] = await db.select().from(messages).where(eq(messages.id, entry.message_id)).limit(1);
|
|
6297
|
+
if (!msg) {
|
|
6298
|
+
await ackEntry(db, entry.id);
|
|
6299
|
+
continue;
|
|
6300
|
+
}
|
|
6301
|
+
const config = inboxToConfig.get(entry.inbox_id);
|
|
6302
|
+
if (!config) {
|
|
6303
|
+
await ackEntry(db, entry.id);
|
|
6304
|
+
continue;
|
|
6305
|
+
}
|
|
6306
|
+
const messageContent = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
6307
|
+
const payload = {
|
|
6308
|
+
hub_chat_id: entry.chat_id ?? msg.chatId,
|
|
6309
|
+
hub_agent_id: config.agentId,
|
|
6310
|
+
hub_server_url: serverUrl,
|
|
6311
|
+
hub_agent_token: config.agentToken,
|
|
6312
|
+
user_id: config.kaelUserId,
|
|
6313
|
+
project_id: config.kaelProjectId,
|
|
6314
|
+
message: messageContent,
|
|
6315
|
+
sender_id: msg.senderId,
|
|
6316
|
+
format: msg.format
|
|
6317
|
+
};
|
|
6318
|
+
const response = await fetch(`${kaelEndpoint}/api/v1/hub/messages`, {
|
|
6319
|
+
method: "POST",
|
|
6320
|
+
headers: {
|
|
6321
|
+
"Content-Type": "application/json",
|
|
6322
|
+
...kaelApiKey ? { "X-Internal-API-Key": kaelApiKey } : {}
|
|
6323
|
+
},
|
|
6324
|
+
body: JSON.stringify(payload)
|
|
6325
|
+
});
|
|
6326
|
+
if (!response.ok) {
|
|
6327
|
+
const body = await response.text().catch(() => "");
|
|
6328
|
+
log.error({
|
|
6329
|
+
entryId: entry.id,
|
|
6330
|
+
status: response.status,
|
|
6331
|
+
body
|
|
6332
|
+
}, "Kael API rejected outbound message");
|
|
6333
|
+
await nackEntry(db, entry.id);
|
|
6334
|
+
errorCount++;
|
|
6335
|
+
continue;
|
|
6336
|
+
}
|
|
6337
|
+
await ackEntry(db, entry.id);
|
|
6338
|
+
sent++;
|
|
6339
|
+
} catch (err) {
|
|
6340
|
+
log.error({
|
|
6341
|
+
entryId: entry.id,
|
|
6342
|
+
err
|
|
6343
|
+
}, "Failed to send outbound Kael message");
|
|
6344
|
+
await nackEntry(db, entry.id).catch((nackErr) => {
|
|
6345
|
+
log.error({
|
|
6346
|
+
entryId: entry.id,
|
|
6347
|
+
err: nackErr
|
|
6348
|
+
}, "Failed to NACK entry");
|
|
6349
|
+
});
|
|
6350
|
+
errorCount++;
|
|
6351
|
+
}
|
|
6352
|
+
} catch (err) {
|
|
6353
|
+
log.error({ err }, "Kael outbound processing error");
|
|
6354
|
+
return {
|
|
6355
|
+
sent: 0,
|
|
6356
|
+
errors: 1
|
|
6357
|
+
};
|
|
6358
|
+
}
|
|
6359
|
+
return {
|
|
6360
|
+
sent,
|
|
6361
|
+
errors: errorCount
|
|
6362
|
+
};
|
|
6363
|
+
},
|
|
6364
|
+
shutdown() {
|
|
6365
|
+
aborted = true;
|
|
6366
|
+
agentConfigs.clear();
|
|
6367
|
+
inboxToConfig.clear();
|
|
6349
6368
|
}
|
|
6350
6369
|
};
|
|
6351
6370
|
}
|
|
6371
|
+
async function ackEntry(db, entryId) {
|
|
6372
|
+
await db.update(inboxEntries).set({
|
|
6373
|
+
status: "acked",
|
|
6374
|
+
ackedAt: /* @__PURE__ */ new Date()
|
|
6375
|
+
}).where(eq(inboxEntries.id, entryId));
|
|
6376
|
+
}
|
|
6377
|
+
const MAX_RETRY_COUNT = 3;
|
|
6378
|
+
async function nackEntry(db, entryId) {
|
|
6379
|
+
await db.execute(sql`
|
|
6380
|
+
UPDATE inbox_entries
|
|
6381
|
+
SET
|
|
6382
|
+
status = CASE WHEN retry_count >= ${MAX_RETRY_COUNT} THEN 'failed' ELSE 'pending' END,
|
|
6383
|
+
retry_count = retry_count + 1
|
|
6384
|
+
WHERE id = ${entryId}
|
|
6385
|
+
`);
|
|
6386
|
+
}
|
|
6352
6387
|
async function buildApp(config) {
|
|
6353
6388
|
const app = Fastify({ logger: config.logger ?? true });
|
|
6354
6389
|
const db = connectDatabase(config.database.url);
|
|
@@ -6455,14 +6490,19 @@ async function buildApp(config) {
|
|
|
6455
6490
|
app.decorate("notifier", notifier);
|
|
6456
6491
|
const adapterManager = createAdapterManager(db, config.secrets.encryptionKey, app.log, notifier);
|
|
6457
6492
|
app.decorate("adapterManager", adapterManager);
|
|
6458
|
-
const
|
|
6493
|
+
const kaelRuntime = config.kael?.endpoint ? createKaelRuntime(db, config.secrets.encryptionKey, config.kael.endpoint, config.kael.apiKey, config.kael.hubPublicUrl, app.log) : void 0;
|
|
6494
|
+
const backgroundTasks = createBackgroundTasks(app, config.instanceId, adapterManager, kaelRuntime);
|
|
6459
6495
|
notifier.onConfigChange((configType) => {
|
|
6460
|
-
if (configType === "adapter_configs")
|
|
6496
|
+
if (configType === "adapter_configs") {
|
|
6497
|
+
adapterManager.reload().catch((err) => app.log.error(err, "Adapter hot-reload failed (PG NOTIFY)"));
|
|
6498
|
+
kaelRuntime?.reload().catch((err) => app.log.error(err, "Kael hot-reload failed (PG NOTIFY)"));
|
|
6499
|
+
}
|
|
6461
6500
|
});
|
|
6462
6501
|
app.addHook("onReady", async () => {
|
|
6463
6502
|
await notifier.start();
|
|
6464
6503
|
backgroundTasks.start();
|
|
6465
6504
|
await adapterManager.reload();
|
|
6505
|
+
await kaelRuntime?.reload();
|
|
6466
6506
|
const { repo: ctRepo, branch: ctBranch, syncInterval } = config.contextTree;
|
|
6467
6507
|
const { token: ghToken } = config.github;
|
|
6468
6508
|
try {
|
|
@@ -6480,6 +6520,7 @@ async function buildApp(config) {
|
|
|
6480
6520
|
app.addHook("onClose", async () => {
|
|
6481
6521
|
backgroundTasks.stop();
|
|
6482
6522
|
adapterManager.shutdown();
|
|
6523
|
+
kaelRuntime?.shutdown();
|
|
6483
6524
|
await notifier.stop();
|
|
6484
6525
|
await listenClient.end();
|
|
6485
6526
|
});
|
|
@@ -6594,4 +6635,4 @@ function resolveWebDist() {
|
|
|
6594
6635
|
} catch {}
|
|
6595
6636
|
}
|
|
6596
6637
|
//#endregion
|
|
6597
|
-
export { ClientRuntime as A,
|
|
6638
|
+
export { ClientRuntime as A, checkWebSocket as C, ensurePostgres as D, status as E, SessionRegistry as F, cleanWorkspaces as I, hasAdminUser as M, FirstTreeHubSDK as N, isDockerAvailable as O, SdkError as P, checkServerReachable as S, blank as T, checkDocker as _, formatCheckReport as a, checkServerConfig as b, onboardContinue as c, runMigrations as d, checkAgentConfigs as f, checkDatabase as g, checkContextTreeRepo as h, promptMissingFields as i, createAdminUser$1 as j, stopPostgres as k, onboardCreate as l, checkClientConfig as m, isInteractive as n, loadOnboardState as o, checkAgentTokens as p, promptAddAgent as r, onboardCheck as s, startServer as t, saveOnboardState as u, checkGitHubToken as v, printResults as w, checkServerHealth as x, checkNodeVersion as y };
|