@agent-team-foundation/first-tree-hub 0.12.4 → 0.12.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/cli/index.mjs +13 -11
- package/dist/{client-D1TDiik_-NV_lkhfI.mjs → client-DL5vHhvQ-CnYGq2x-.mjs} +23 -4
- package/dist/{client-0RrgrMjR-CylTJGEb.mjs → client-DSM_opoz-BH5eegXb.mjs} +2 -2
- package/dist/{dist-CMhywpXB.mjs → dist-BwPlBZWi.mjs} +26 -2
- package/dist/drizzle/0036_github_entity_chat_mappings.sql +47 -0
- package/dist/drizzle/meta/_journal.json +7 -0
- package/dist/{feishu-tkZS0vvL.mjs → feishu-CKGzIamp.mjs} +1 -1
- package/dist/index.mjs +4 -4
- package/dist/{invitation-C299fxkP-CZRV665C.mjs → invitation-C299fxkP-Dts66QTU.mjs} +1 -1
- package/dist/{saas-connect-S71rG182.mjs → saas-connect-DYjvx5yr.mjs} +507 -236
- package/dist/web/assets/index-BXDLOc-s.js +406 -0
- package/dist/web/assets/{index-RNegidl2.js → index-Dyo6TAWC.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-BG9RRx2e.js +0 -401
|
@@ -2,10 +2,10 @@ import { a as __toCommonJS, o as __toESM, t as __commonJSMin } from "./chunk-BSw
|
|
|
2
2
|
import { A as FIRST_TREE_HUB_ATTR, C as stampOrgScope, D as untrustedAttrs, E as startWsConnectionSpan, M as require_pino, O as withSpan, S as stampChatResource, _ as rootLogger$1, a as buildRateLimitError, c as currentTraceId, g as reportErrorToRoot, i as bodyCaptureOnSendHook, j as redactUrl, k as withWsMessageSpan, l as decodeJwtForTrace, m as observabilityPlugin, n as applyLoggerConfig, o as classifyJoseError, r as attachRequestContext, s as createLogger$1, t as adapterAttrs, u as endWsConnectionSpan, x as stampAgentResource, y as setWsConnectionAttrs } from "./observability-BAScT_5S-BcW9HgkG.mjs";
|
|
3
3
|
import { C as resetConfigMeta, E as setConfigValue, S as resetConfig, T as serverConfigSchema, b as migrateLegacyHome, c as resolveServerUrl, d as DEFAULT_CONFIG_DIR, f as DEFAULT_DATA_DIR$1, g as collectMissingPrompts, h as clientConfigSchema, i as ensureFreshAccessToken, l as saveAgentConfig, m as agentConfigSchema, o as loadCredentials, p as DEFAULT_HOME_DIR$1, u as saveCredentials, v as initConfig, w as resolveConfigReadonly, y as loadAgents } from "./bootstrap-C_K2CKXC.mjs";
|
|
4
4
|
import { a as print, i as blank, n as CLI_USER_AGENT, r as COMMAND_VERSION, s as status, t as cliFetch } from "./cli-fetch--tiwKm5S.mjs";
|
|
5
|
-
import { $ as
|
|
5
|
+
import { $ as refreshTokenSchema, A as dryRunAgentRuntimeConfigSchema, B as isRedactedEnvValue, C as createAgentSchema, D as createOrgFromMeSchema, E as createMemberSchema, F as imageInlineContentSchema, G as messageSourceSchema$1, H as joinByInvitationSchema, I as inboxAckFrameSchema, J as paginationQuerySchema, K as notificationQuerySchema, L as inboxDeliverFrameSchema$1, M as githubCallbackQuerySchema, N as githubDevCallbackQuerySchema, O as defaultRuntimeConfigPayload, P as githubStartQuerySchema, Q as rebindAgentSchema, R as inboxPollQuerySchema, S as createAdapterMappingSchema, T as createMeChatSchema, U as listMeChatsQuerySchema, V as isReservedAgentName$1, W as loginSchema, Y as patchOnboardingSchema, _t as updateClientCapabilitiesSchema, a as AGENT_VISIBILITY, at as sendToAgentSchema, b as contextTreeSnapshotSchema, bt as wsAuthFrameSchema, c as ORG_SETTINGS_NAMESPACES$1, ct as sessionEventSchema$1, d as addParticipantSchema, dt as stripCode, et as runtimeStateMessageSchema, f as agentBindRequestSchema, ft as submitQuestionAnswerSchema, g as chatMetadataSchema$1, gt as updateChatSchema, h as agentTypeSchema$1, ht as updateAgentSchema, i as AGENT_STATUSES, it as sendMessageSchema, k as delegateFeishuUserSchema, l as WS_AUTH_FRAME_TIMEOUT_MS, lt as sessionReconcileRequestSchema, m as agentRuntimeConfigPayloadSchema$1, mt as updateAgentRuntimeConfigSchema, n as AGENT_NAME_REGEX$1, ot as sessionCompletionMessageSchema, p as agentPinnedMessageSchema$1, pt as updateAdapterConfigSchema, q as onboardingEventSchema, r as AGENT_SELECTOR_HEADER$1, rt as selfServiceFeishuBotSchema, s as MENTION_REGEX, st as sessionEventMessageSchema, t as AGENT_BIND_REJECT_REASONS, tt as safeRedirectPath, u as addMeChatParticipantsSchema, ut as sessionStateMessageSchema, v as clientRegisterSchema, vt as updateMemberSchema, w as createChatSchema, x as createAdapterConfigSchema, y as connectTokenExchangeSchema, yt as updateOrganizationSchema, z as isOrgSettingNamespace } from "./dist-BwPlBZWi.mjs";
|
|
6
6
|
import { a as ConflictError, c as UnauthorizedError, i as ClientUserMismatchError$1, l as organizations, n as BadRequestError, o as ForbiddenError, r as ClientOrgMismatchError$1, s as NotFoundError, t as AppError, u as users } from "./errors-CF5evtJt-B0NTIVPt.mjs";
|
|
7
7
|
import { n as init_esm, r as trace, t as esm_exports } from "./esm-iadMkGbV.mjs";
|
|
8
|
-
import { $ as pendingQuestions, A as heartbeatClient, B as listAgentsWithRuntime, C as findOrCreateDirectChat, D as getClient, E as getChatDetail, F as joinChat, G as listClientsForOrgAdmin, H as listChats, I as leaveAsParticipant, J as markStaleAgents, K as listMessages, L as leaveChat, M as inboxEntries, N as invalidateChatAudience, O as getOnlineCount, P as joinAsParticipant, Q as notifyRecipients, R as listActiveAgentsPinnedToClient, S as ensureParticipant$1, T as getCachedAudience, U as listChatsForMember, V as listChatParticipantsWithNames, W as listClients, X as members, Y as markSupersededByChat, Z as messages, _ as createNotifier, _t as updateClientCapabilities, a as agents, at as removeParticipant, b as editMessage, c as bindAgent, ct as retireClient, d as chats, dt as serverInstances, et as recomputeChatWatchers, f as claimClient, ft as setOffline, g as createChat, gt as unbindAgent, h as clients, ht as touchAgent, i as agentVisibilityCondition, it as registerClient, j as heartbeatInstance, k as getPresence, l as chatParticipants, lt as sendMessage, m as cleanupStalePresence, mt as submitAnswer, n as agentChatSessions, nt as recomputeWatchersForMember, o as assertClientOwner, ot as resetActivity, p as cleanupStaleClients, pt as setRuntimeState, r as agentPresence, rt as registerChatMessageDispatcher, s as assertParticipant, st as resolveChatMembership, t as addParticipant, tt as recomputeWatchersForAgent, u as chatSubscriptions, ut as sendToAgent$1, v as deriveAuthState, vt as upsertSessionState, w as getActivityOverview, x as ensureCanJoin, y as disconnectClient, z as listAgentsManagedByUser } from "./client-
|
|
8
|
+
import { $ as pendingQuestions, A as heartbeatClient, B as listAgentsWithRuntime, C as findOrCreateDirectChat, D as getClient, E as getChatDetail, F as joinChat, G as listClientsForOrgAdmin, H as listChats, I as leaveAsParticipant, J as markStaleAgents, K as listMessages, L as leaveChat, M as inboxEntries, N as invalidateChatAudience, O as getOnlineCount, P as joinAsParticipant, Q as notifyRecipients, R as listActiveAgentsPinnedToClient, S as ensureParticipant$1, T as getCachedAudience, U as listChatsForMember, V as listChatParticipantsWithNames, W as listClients, X as members, Y as markSupersededByChat, Z as messages, _ as createNotifier, _t as updateClientCapabilities, a as agents, at as removeParticipant, b as editMessage, c as bindAgent, ct as retireClient, d as chats, dt as serverInstances, et as recomputeChatWatchers, f as claimClient, ft as setOffline, g as createChat, gt as unbindAgent, h as clients, ht as touchAgent, i as agentVisibilityCondition, it as registerClient, j as heartbeatInstance, k as getPresence, l as chatParticipants, lt as sendMessage, m as cleanupStalePresence, mt as submitAnswer, n as agentChatSessions, nt as recomputeWatchersForMember, o as assertClientOwner, ot as resetActivity, p as cleanupStaleClients, pt as setRuntimeState, r as agentPresence, rt as registerChatMessageDispatcher, s as assertParticipant, st as resolveChatMembership, t as addParticipant, tt as recomputeWatchersForAgent, u as chatSubscriptions, ut as sendToAgent$1, v as deriveAuthState, vt as upsertSessionState, w as getActivityOverview, x as ensureCanJoin, y as disconnectClient, z as listAgentsManagedByUser } from "./client-DL5vHhvQ-CnYGq2x-.mjs";
|
|
9
9
|
import { a as invitationRedemptions, c as recordRedemption, i as getActiveInvitation, l as rotateInvitation, n as ensureActiveInvitation, o as invitations, r as findActiveByToken, t as buildInviteUrl, u as uuidv7 } from "./invitation-Bg0TRiyx-BsZH4GCS.mjs";
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
import { ZodError, z } from "zod";
|
|
@@ -730,6 +730,30 @@ z.object({
|
|
|
730
730
|
expiresIn: z.number(),
|
|
731
731
|
command: z.string()
|
|
732
732
|
});
|
|
733
|
+
const githubEntityTypeSchema = z.enum([
|
|
734
|
+
"issue",
|
|
735
|
+
"pull_request",
|
|
736
|
+
"discussion",
|
|
737
|
+
"commit"
|
|
738
|
+
]);
|
|
739
|
+
const githubChatMetadataSchema = z.object({
|
|
740
|
+
source: z.literal("github"),
|
|
741
|
+
entityType: githubEntityTypeSchema,
|
|
742
|
+
entityKey: z.string().min(1),
|
|
743
|
+
entityUrl: z.string().url().optional()
|
|
744
|
+
});
|
|
745
|
+
const feishuChatMetadataSchema = z.object({
|
|
746
|
+
source: z.literal("feishu"),
|
|
747
|
+
externalChannelId: z.string().min(1)
|
|
748
|
+
});
|
|
749
|
+
const chatMetadataSchema = z.discriminatedUnion("source", [githubChatMetadataSchema, feishuChatMetadataSchema]);
|
|
750
|
+
/**
|
|
751
|
+
* `createChat` callers may not set metadata at all (admin-created group chats,
|
|
752
|
+
* me-chats, …), so the input schema accepts either an empty object or one of
|
|
753
|
+
* the typed variants. The empty `{}` arm is `.strict()` so a caller cannot
|
|
754
|
+
* sneak through `{ source: "github" }` without the required fields.
|
|
755
|
+
*/
|
|
756
|
+
const optionalChatMetadataSchema = z.union([z.object({}).strict(), chatMetadataSchema]);
|
|
733
757
|
const chatTypeSchema = z.enum([
|
|
734
758
|
"direct",
|
|
735
759
|
"group",
|
|
@@ -739,7 +763,7 @@ z.object({
|
|
|
739
763
|
type: chatTypeSchema,
|
|
740
764
|
topic: z.string().max(500).optional(),
|
|
741
765
|
participantIds: z.array(z.string()).min(1),
|
|
742
|
-
metadata:
|
|
766
|
+
metadata: optionalChatMetadataSchema.optional()
|
|
743
767
|
});
|
|
744
768
|
const chatParticipantSchema = z.object({
|
|
745
769
|
agentId: z.string(),
|
|
@@ -3031,7 +3055,8 @@ function generateToolsDoc() {
|
|
|
3031
3055
|
You are running inside **Agent Hub**, a messaging platform for agent teams.
|
|
3032
3056
|
|
|
3033
3057
|
- Messages from other team members arrive as your prompt input
|
|
3034
|
-
- Each message includes a \`[From:
|
|
3058
|
+
- Each message includes a \`[From: <agent-name>]\` header — that name is also
|
|
3059
|
+
what you pass back to \`agent send\` to reply to or address that agent
|
|
3035
3060
|
- **Your final text response is automatically delivered** to the chat — just respond normally
|
|
3036
3061
|
- For **proactive communication** (sending to other agents, other chats, or structured data),
|
|
3037
3062
|
use the \`first-tree-hub\` CLI below
|
|
@@ -3046,8 +3071,8 @@ These are injected automatically when the agent process starts:
|
|
|
3046
3071
|
|----------|-------------|
|
|
3047
3072
|
| \`FIRST_TREE_HUB_SERVER_URL\` | Server address for API calls |
|
|
3048
3073
|
| \`FIRST_TREE_HUB_ACCESS_TOKEN\` | User member access JWT (short-lived) |
|
|
3049
|
-
| \`FIRST_TREE_HUB_AGENT_ID\` |
|
|
3050
|
-
| \`FIRST_TREE_HUB_CHAT_ID\` |
|
|
3074
|
+
| \`FIRST_TREE_HUB_AGENT_ID\` | YOUR own agent UUID. The CLI reads it to identify you as the sender — never pass it as a \`send\` target. |
|
|
3075
|
+
| \`FIRST_TREE_HUB_CHAT_ID\` | The chat this session is currently bound to. The CLI uses it to route messages — you don't need to pass it manually. |
|
|
3051
3076
|
|
|
3052
3077
|
The \`first-tree-hub\` CLI reads these automatically — no extra setup needed.
|
|
3053
3078
|
|
|
@@ -3057,13 +3082,18 @@ Use the \`first-tree-hub agent send\` CLI — it reads the env vars above and
|
|
|
3057
3082
|
attaches the \`Authorization\` + \`X-Agent-Id\` headers automatically:
|
|
3058
3083
|
|
|
3059
3084
|
\`\`\`bash
|
|
3060
|
-
# Send to another agent —
|
|
3061
|
-
#
|
|
3085
|
+
# Send to another agent — first positional argument is the recipient's NAME
|
|
3086
|
+
# (NOT a uuid; uuids in chat history / participant lists are not accepted).
|
|
3062
3087
|
# Run \`first-tree-hub agent list\` to see available names.
|
|
3088
|
+
#
|
|
3089
|
+
# Routing: if the recipient is a participant of your current chat (typically
|
|
3090
|
+
# the case in a group chat where someone @-mentioned you to talk to them),
|
|
3091
|
+
# the message stays in that chat. Otherwise it falls back to a direct chat
|
|
3092
|
+
# between you and the recipient. You don't need to think about which.
|
|
3063
3093
|
first-tree-hub agent send <agentName> "your message"
|
|
3064
3094
|
|
|
3065
|
-
# Send
|
|
3066
|
-
#
|
|
3095
|
+
# Send into a specific chat by id — use this only when you explicitly want
|
|
3096
|
+
# to address a chat your current session is NOT bound to.
|
|
3067
3097
|
first-tree-hub agent send --chat <chatId> "your message"
|
|
3068
3098
|
|
|
3069
3099
|
# Send markdown (default format is text)
|
|
@@ -6922,6 +6952,36 @@ function resolveReplyToFromEnv(env, override) {
|
|
|
6922
6952
|
replyToChat: override.replyToChat ?? (envComplete ? envChatId : void 0)
|
|
6923
6953
|
};
|
|
6924
6954
|
}
|
|
6955
|
+
function resolveSenderName(input) {
|
|
6956
|
+
const { override, envAgentId, agents } = input;
|
|
6957
|
+
if (agents.size === 0) return { kind: "none" };
|
|
6958
|
+
if (override) return {
|
|
6959
|
+
kind: "ok",
|
|
6960
|
+
name: override
|
|
6961
|
+
};
|
|
6962
|
+
if (envAgentId) {
|
|
6963
|
+
for (const [name, cfg] of agents) if (cfg.agentId === envAgentId) return {
|
|
6964
|
+
kind: "ok",
|
|
6965
|
+
name
|
|
6966
|
+
};
|
|
6967
|
+
return {
|
|
6968
|
+
kind: "envMismatch",
|
|
6969
|
+
envAgentId,
|
|
6970
|
+
available: [...agents.keys()]
|
|
6971
|
+
};
|
|
6972
|
+
}
|
|
6973
|
+
if (agents.size === 1) {
|
|
6974
|
+
const [only] = [...agents.keys()];
|
|
6975
|
+
if (only) return {
|
|
6976
|
+
kind: "ok",
|
|
6977
|
+
name: only
|
|
6978
|
+
};
|
|
6979
|
+
}
|
|
6980
|
+
return {
|
|
6981
|
+
kind: "ambiguous",
|
|
6982
|
+
available: [...agents.keys()]
|
|
6983
|
+
};
|
|
6984
|
+
}
|
|
6925
6985
|
//#endregion
|
|
6926
6986
|
//#region src/core/admin.ts
|
|
6927
6987
|
/**
|
|
@@ -9091,7 +9151,7 @@ async function onboardCreate(args) {
|
|
|
9091
9151
|
}
|
|
9092
9152
|
const runtimeAgent = args.type === "human" ? args.assistant : args.id;
|
|
9093
9153
|
if (args.feishuBotAppId && args.feishuBotAppSecret) {
|
|
9094
|
-
const { bindFeishuBot } = await import("./feishu-
|
|
9154
|
+
const { bindFeishuBot } = await import("./feishu-CKGzIamp.mjs").then((n) => n.r);
|
|
9095
9155
|
const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
|
|
9096
9156
|
if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
|
|
9097
9157
|
else {
|
|
@@ -10304,7 +10364,7 @@ function createFeedbackHandler(config) {
|
|
|
10304
10364
|
return { handle };
|
|
10305
10365
|
}
|
|
10306
10366
|
//#endregion
|
|
10307
|
-
//#region ../server/dist/app-
|
|
10367
|
+
//#region ../server/dist/app-kJNM9Cf1.mjs
|
|
10308
10368
|
var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
|
|
10309
10369
|
init_esm();
|
|
10310
10370
|
var __defProp = Object.defineProperty;
|
|
@@ -11470,16 +11530,17 @@ async function findOrCreateChatForChannel(db, data) {
|
|
|
11470
11530
|
return db.transaction(async (tx) => {
|
|
11471
11531
|
const [botAgent] = await tx.select({ organizationId: agents.organizationId }).from(agents).where(eq(agents.uuid, data.botAgentId)).limit(1);
|
|
11472
11532
|
const orgId = botAgent?.organizationId ?? await resolveDefaultOrgId(db);
|
|
11533
|
+
const metadata = chatMetadataSchema$1.parse({
|
|
11534
|
+
source: data.platform,
|
|
11535
|
+
externalChannelId: data.externalChannelId
|
|
11536
|
+
});
|
|
11473
11537
|
await tx.insert(chats).values({
|
|
11474
11538
|
id: chatId,
|
|
11475
11539
|
organizationId: orgId,
|
|
11476
11540
|
type: internalType,
|
|
11477
11541
|
topic: data.topic ?? null,
|
|
11478
11542
|
lifecyclePolicy: "adapter_managed",
|
|
11479
|
-
metadata
|
|
11480
|
-
source: data.platform,
|
|
11481
|
-
externalChannelId: data.externalChannelId
|
|
11482
|
-
}
|
|
11543
|
+
metadata
|
|
11483
11544
|
});
|
|
11484
11545
|
const participants = data.botAgentId === data.senderAgentId ? [{
|
|
11485
11546
|
chatId,
|
|
@@ -16403,7 +16464,7 @@ async function healthzRoutes(app) {
|
|
|
16403
16464
|
* `api/orgs/invitations.ts` (Class B, admin-gated).
|
|
16404
16465
|
*/
|
|
16405
16466
|
async function publicInvitationRoutes(app) {
|
|
16406
|
-
const { previewInvitation } = await import("./invitation-C299fxkP-
|
|
16467
|
+
const { previewInvitation } = await import("./invitation-C299fxkP-Dts66QTU.mjs");
|
|
16407
16468
|
app.get("/:token/preview", async (request, reply) => {
|
|
16408
16469
|
if (!request.params.token) throw new UnauthorizedError("Token required");
|
|
16409
16470
|
const preview = await previewInvitation(app.db, request.params.token);
|
|
@@ -16583,7 +16644,7 @@ async function meRoutes(app) {
|
|
|
16583
16644
|
*/
|
|
16584
16645
|
app.get("/me/pinned-agents", async (request) => {
|
|
16585
16646
|
const { userId } = requireUser(request);
|
|
16586
|
-
const { listMyPinnedAgents } = await import("./client-
|
|
16647
|
+
const { listMyPinnedAgents } = await import("./client-DSM_opoz-BH5eegXb.mjs");
|
|
16587
16648
|
return listMyPinnedAgents(app.db, { userId });
|
|
16588
16649
|
});
|
|
16589
16650
|
/**
|
|
@@ -17616,58 +17677,379 @@ async function sessionRoutes(app) {
|
|
|
17616
17677
|
});
|
|
17617
17678
|
});
|
|
17618
17679
|
}
|
|
17619
|
-
|
|
17620
|
-
|
|
17621
|
-
function verifySignature(secret, rawBody, signatureHeader) {
|
|
17622
|
-
const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
|
|
17623
|
-
const expectedBuf = Buffer.from(expected, "utf8");
|
|
17624
|
-
const receivedBuf = Buffer.from(signatureHeader, "utf8");
|
|
17625
|
-
if (expectedBuf.length !== receivedBuf.length || !timingSafeEqual(expectedBuf, receivedBuf)) throw new UnauthorizedError("Invalid webhook signature");
|
|
17680
|
+
function isRecord(value) {
|
|
17681
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17626
17682
|
}
|
|
17627
|
-
|
|
17628
|
-
|
|
17629
|
-
if (
|
|
17630
|
-
|
|
17631
|
-
|
|
17632
|
-
|
|
17633
|
-
|
|
17634
|
-
|
|
17635
|
-
|
|
17636
|
-
|
|
17637
|
-
|
|
17638
|
-
|
|
17639
|
-
|
|
17640
|
-
|
|
17641
|
-
|
|
17642
|
-
|
|
17643
|
-
|
|
17644
|
-
|
|
17683
|
+
/** Pull `repository.full_name` ("owner/repo") from a webhook payload, or null. */
|
|
17684
|
+
function repoFullName(payload) {
|
|
17685
|
+
if (!isRecord(payload)) return null;
|
|
17686
|
+
const repo = isRecord(payload.repository) ? payload.repository : null;
|
|
17687
|
+
return typeof repo?.full_name === "string" && repo.full_name.length > 0 ? repo.full_name : null;
|
|
17688
|
+
}
|
|
17689
|
+
function readNumber(value) {
|
|
17690
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
17691
|
+
}
|
|
17692
|
+
function readString(value) {
|
|
17693
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
17694
|
+
}
|
|
17695
|
+
/**
|
|
17696
|
+
* Resolve the entity that a GitHub webhook event belongs to.
|
|
17697
|
+
*
|
|
17698
|
+
* Returns `null` when the event isn't a clustering candidate (event type
|
|
17699
|
+
* outside the §4.1 "core" list, malformed payload). Caller is expected to
|
|
17700
|
+
* skip such events.
|
|
17701
|
+
*
|
|
17702
|
+
* Notes
|
|
17703
|
+
* - `commit_comment` falls back to a `commit` entity keyed on `<repo>@<sha>`
|
|
17704
|
+
* when no associated PR is in the payload — the design hedges on "optionally
|
|
17705
|
+
* resolve to a PR", but doing so requires an extra GitHub API call which we
|
|
17706
|
+
* defer to Phase 1+.
|
|
17707
|
+
*/
|
|
17708
|
+
function extractEventEntity(eventType, payload) {
|
|
17709
|
+
if (!isRecord(payload)) return null;
|
|
17710
|
+
const repo = repoFullName(payload);
|
|
17711
|
+
if (!repo) return null;
|
|
17712
|
+
switch (eventType) {
|
|
17713
|
+
case "issues":
|
|
17714
|
+
case "issue_comment": {
|
|
17715
|
+
const issue = isRecord(payload.issue) ? payload.issue : null;
|
|
17716
|
+
const number = readNumber(issue?.number);
|
|
17717
|
+
if (number === null) return null;
|
|
17718
|
+
return {
|
|
17719
|
+
type: "issue",
|
|
17720
|
+
key: `${repo}#${number}`,
|
|
17721
|
+
title: readString(issue?.title) ?? void 0,
|
|
17722
|
+
url: readString(issue?.html_url) ?? void 0
|
|
17723
|
+
};
|
|
17645
17724
|
}
|
|
17646
|
-
|
|
17725
|
+
case "pull_request":
|
|
17726
|
+
case "pull_request_review":
|
|
17727
|
+
case "pull_request_review_comment": {
|
|
17728
|
+
const pr = isRecord(payload.pull_request) ? payload.pull_request : null;
|
|
17729
|
+
const number = readNumber(pr?.number);
|
|
17730
|
+
if (number === null) return null;
|
|
17731
|
+
return {
|
|
17732
|
+
type: "pull_request",
|
|
17733
|
+
key: `${repo}#${number}`,
|
|
17734
|
+
title: readString(pr?.title) ?? void 0,
|
|
17735
|
+
url: readString(pr?.html_url) ?? void 0
|
|
17736
|
+
};
|
|
17737
|
+
}
|
|
17738
|
+
case "discussion":
|
|
17739
|
+
case "discussion_comment": {
|
|
17740
|
+
const disc = isRecord(payload.discussion) ? payload.discussion : null;
|
|
17741
|
+
const number = readNumber(disc?.number);
|
|
17742
|
+
if (number === null) return null;
|
|
17743
|
+
return {
|
|
17744
|
+
type: "discussion",
|
|
17745
|
+
key: `${repo}#discussion-${number}`,
|
|
17746
|
+
title: readString(disc?.title) ?? void 0,
|
|
17747
|
+
url: readString(disc?.html_url) ?? void 0
|
|
17748
|
+
};
|
|
17749
|
+
}
|
|
17750
|
+
case "commit_comment": {
|
|
17751
|
+
const comment = isRecord(payload.comment) ? payload.comment : null;
|
|
17752
|
+
const sha = readString(comment?.commit_id);
|
|
17753
|
+
if (!sha) return null;
|
|
17754
|
+
return {
|
|
17755
|
+
type: "commit",
|
|
17756
|
+
key: `${repo}@${sha}`,
|
|
17757
|
+
url: readString(comment?.html_url) ?? void 0
|
|
17758
|
+
};
|
|
17759
|
+
}
|
|
17760
|
+
default: return null;
|
|
17647
17761
|
}
|
|
17648
17762
|
}
|
|
17649
|
-
|
|
17650
|
-
|
|
17651
|
-
|
|
17652
|
-
|
|
17653
|
-
|
|
17654
|
-
|
|
17655
|
-
|
|
17656
|
-
|
|
17657
|
-
|
|
17658
|
-
|
|
17659
|
-
|
|
17660
|
-
|
|
17661
|
-
|
|
17662
|
-
|
|
17663
|
-
|
|
17664
|
-
|
|
17665
|
-
|
|
17763
|
+
/**
|
|
17764
|
+
* Closing-keyword regex from
|
|
17765
|
+
* https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue
|
|
17766
|
+
* — `close[sd]? | fix(es|ed)? | resolve[sd]?`. Cross-repo `org/repo#N` is
|
|
17767
|
+
* deliberately excluded (out of scope for Phase 0; see §4.5).
|
|
17768
|
+
*/
|
|
17769
|
+
const FIXES_KEYWORDS_RE = /\b(?:close[sd]?|fix(?:es|ed)?|resolve[sd]?)\s+#(\d+)\b/gi;
|
|
17770
|
+
/**
|
|
17771
|
+
* Parse `Fixes #N` / `Closes #N` / `Resolves #N` references out of a PR body.
|
|
17772
|
+
* Returns ordered, deduplicated entity references for issues in the same repo
|
|
17773
|
+
* (cross-repo refs ignored per §4.5).
|
|
17774
|
+
*
|
|
17775
|
+
* Caller is expected to pass `repoFullName` so we can build the entity key.
|
|
17776
|
+
*/
|
|
17777
|
+
function parseFixesRefs(text, repoFullName) {
|
|
17778
|
+
if (!text) return [];
|
|
17779
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17780
|
+
const out = [];
|
|
17781
|
+
for (const match of text.matchAll(FIXES_KEYWORDS_RE)) {
|
|
17782
|
+
const num = match[1];
|
|
17783
|
+
if (!num) continue;
|
|
17784
|
+
const key = `${repoFullName}#${num}`;
|
|
17785
|
+
if (seen.has(key)) continue;
|
|
17786
|
+
seen.add(key);
|
|
17787
|
+
out.push({
|
|
17788
|
+
type: "issue",
|
|
17789
|
+
key
|
|
17790
|
+
});
|
|
17666
17791
|
}
|
|
17667
|
-
return
|
|
17792
|
+
return out;
|
|
17668
17793
|
}
|
|
17669
|
-
|
|
17670
|
-
|
|
17794
|
+
/**
|
|
17795
|
+
* Pick a chat-title prefix from (entity, eventType, action).
|
|
17796
|
+
*
|
|
17797
|
+
* PR review-flow events (`pull_request.review_requested`,
|
|
17798
|
+
* `pull_request_review.*`, `pull_request_review_comment.*`) collapse into a
|
|
17799
|
+
* single "PR Review" prefix so a chat first-touched by a review event is
|
|
17800
|
+
* visibly distinct from one first-touched by `pull_request.opened`. Everything
|
|
17801
|
+
* else just renders the entity type.
|
|
17802
|
+
*
|
|
17803
|
+
* Note: chat titles are written once at chat creation (see
|
|
17804
|
+
* `github-entity-chat.ts::createEntityChat`) — subsequent events for the same
|
|
17805
|
+
* entity reuse the existing title even if their (event, action) maps to a
|
|
17806
|
+
* different prefix. This matches the "entity is the container" semantic.
|
|
17807
|
+
*/
|
|
17808
|
+
function entityTitlePrefix(entity, eventType, action) {
|
|
17809
|
+
if (eventType === "pull_request" && action === "review_requested") return "PR Review";
|
|
17810
|
+
if (eventType === "pull_request_review") return "PR Review";
|
|
17811
|
+
if (eventType === "pull_request_review_comment") return "PR Review";
|
|
17812
|
+
switch (entity.type) {
|
|
17813
|
+
case "issue": return "Issue";
|
|
17814
|
+
case "pull_request": return "PR";
|
|
17815
|
+
case "discussion": return "Discussion";
|
|
17816
|
+
case "commit": return "Commit";
|
|
17817
|
+
}
|
|
17818
|
+
}
|
|
17819
|
+
/**
|
|
17820
|
+
* Strip the leading `owner/` segment from an entity key so the chat title
|
|
17821
|
+
* stays compact. `owner/repo#42` → `repo#42`; `owner/repo@abc1234` →
|
|
17822
|
+
* `repo@abc1234`. The full `owner/repo#N` form is still used as the
|
|
17823
|
+
* clustering primary key (`github_entity_chat_mappings.entity_key`); only the
|
|
17824
|
+
* display string is shortened.
|
|
17825
|
+
*/
|
|
17826
|
+
function shortEntityKey(key) {
|
|
17827
|
+
const slash = key.indexOf("/");
|
|
17828
|
+
return slash === -1 ? key : key.slice(slash + 1);
|
|
17829
|
+
}
|
|
17830
|
+
/**
|
|
17831
|
+
* Render a chat topic from an entity. Used as the chat title; kept short so
|
|
17832
|
+
* the chat-list row doesn't truncate aggressively.
|
|
17833
|
+
*
|
|
17834
|
+
* formatEntityTitle({ type: "pull_request", key: "owner/repo#307", title: "Improve overview" }, "pull_request", "opened")
|
|
17835
|
+
* → "PR repo#307: Improve overview"
|
|
17836
|
+
* formatEntityTitle(<same>, "pull_request", "review_requested")
|
|
17837
|
+
* → "PR Review repo#307: Improve overview"
|
|
17838
|
+
*/
|
|
17839
|
+
function formatEntityTitle(entity, eventType, action) {
|
|
17840
|
+
const head = `${entityTitlePrefix(entity, eventType, action)} ${shortEntityKey(entity.key)}`;
|
|
17841
|
+
if (entity.title && entity.title.length > 0) return `${head}: ${entity.title}`;
|
|
17842
|
+
return head;
|
|
17843
|
+
}
|
|
17844
|
+
const SILENT_EVENT_TYPES = new Set([
|
|
17845
|
+
"workflow_run",
|
|
17846
|
+
"workflow_job",
|
|
17847
|
+
"check_run",
|
|
17848
|
+
"check_suite",
|
|
17849
|
+
"status",
|
|
17850
|
+
"push",
|
|
17851
|
+
"create",
|
|
17852
|
+
"delete",
|
|
17853
|
+
"fork",
|
|
17854
|
+
"watch",
|
|
17855
|
+
"release",
|
|
17856
|
+
"label",
|
|
17857
|
+
"label_created",
|
|
17858
|
+
"label_deleted",
|
|
17859
|
+
"reaction",
|
|
17860
|
+
"member",
|
|
17861
|
+
"membership",
|
|
17862
|
+
"team",
|
|
17863
|
+
"team_add",
|
|
17864
|
+
"organization",
|
|
17865
|
+
"org_block",
|
|
17866
|
+
"project",
|
|
17867
|
+
"project_card",
|
|
17868
|
+
"project_column"
|
|
17869
|
+
]);
|
|
17870
|
+
/**
|
|
17871
|
+
* Per-event-type action-level filters. Frequent low-signal actions that would
|
|
17872
|
+
* otherwise spam an entity chat. `synchronize` (PR branch push) is the most
|
|
17873
|
+
* common offender — it fires on every commit push to a PR branch and never
|
|
17874
|
+
* carries new conversation.
|
|
17875
|
+
*/
|
|
17876
|
+
const SILENT_ACTIONS = {
|
|
17877
|
+
issues: new Set([
|
|
17878
|
+
"labeled",
|
|
17879
|
+
"unlabeled",
|
|
17880
|
+
"milestoned",
|
|
17881
|
+
"demilestoned",
|
|
17882
|
+
"pinned",
|
|
17883
|
+
"unpinned"
|
|
17884
|
+
]),
|
|
17885
|
+
pull_request: new Set([
|
|
17886
|
+
"labeled",
|
|
17887
|
+
"unlabeled",
|
|
17888
|
+
"auto_merge_enabled",
|
|
17889
|
+
"auto_merge_disabled",
|
|
17890
|
+
"synchronize"
|
|
17891
|
+
])
|
|
17892
|
+
};
|
|
17893
|
+
/** True iff the event should be silently 200-OKed without further routing. */
|
|
17894
|
+
function shouldSilent(eventType, payload) {
|
|
17895
|
+
if (SILENT_EVENT_TYPES.has(eventType)) return true;
|
|
17896
|
+
if (!isRecord(payload)) return false;
|
|
17897
|
+
if (readString((isRecord(payload.sender) ? payload.sender : null)?.type) === "Bot") return true;
|
|
17898
|
+
const action = readString(payload.action);
|
|
17899
|
+
if (!action) return false;
|
|
17900
|
+
return SILENT_ACTIONS[eventType]?.has(action) ?? false;
|
|
17901
|
+
}
|
|
17902
|
+
/**
|
|
17903
|
+
* GitHub-specific webhook entity → chat clustering (Phase 0).
|
|
17904
|
+
*
|
|
17905
|
+
* Each `(organization, human_agent, delegate_agent, entity)` tuple resolves to
|
|
17906
|
+
* exactly one chat. Future external sources (Linear, Slack, …) get their own
|
|
17907
|
+
* tables — their entity models differ enough that a generic table would slip
|
|
17908
|
+
* back into untyped jsonb.
|
|
17909
|
+
*
|
|
17910
|
+
* `bound_via` distinguishes the first-touch row (`direct`) from a row written
|
|
17911
|
+
* by the `Fixes #N` linker (`fixes_link`). Routing logic ignores the
|
|
17912
|
+
* distinction; it exists for audit and future strategy tweaks.
|
|
17913
|
+
*/
|
|
17914
|
+
const githubEntityChatMappings = pgTable("github_entity_chat_mappings", {
|
|
17915
|
+
organizationId: text("organization_id").notNull().references(() => organizations.id),
|
|
17916
|
+
humanAgentId: text("human_agent_id").notNull().references(() => agents.uuid, { onDelete: "cascade" }),
|
|
17917
|
+
delegateAgentId: text("delegate_agent_id").notNull().references(() => agents.uuid, { onDelete: "cascade" }),
|
|
17918
|
+
entityType: text("entity_type").notNull(),
|
|
17919
|
+
entityKey: text("entity_key").notNull(),
|
|
17920
|
+
chatId: text("chat_id").notNull().references(() => chats.id, { onDelete: "cascade" }),
|
|
17921
|
+
boundAt: timestamp("bound_at", { withTimezone: true }).notNull().defaultNow(),
|
|
17922
|
+
boundVia: text("bound_via").notNull()
|
|
17923
|
+
}, (table) => [primaryKey({ columns: [
|
|
17924
|
+
table.organizationId,
|
|
17925
|
+
table.humanAgentId,
|
|
17926
|
+
table.delegateAgentId,
|
|
17927
|
+
table.entityType,
|
|
17928
|
+
table.entityKey
|
|
17929
|
+
] }), index("idx_github_entity_chat_mappings_chat").on(table.chatId)]);
|
|
17930
|
+
/**
|
|
17931
|
+
* Resolve which chat a GitHub event for (human, delegate, entity) belongs to.
|
|
17932
|
+
*
|
|
17933
|
+
* Three-step strategy from docs/webhook-routing-design.md §4.4:
|
|
17934
|
+
* a. Direct hit — entity already bound; reuse that chat.
|
|
17935
|
+
* b. Fixes-link — any related entity (parsed from `Fixes #N` in a PR body)
|
|
17936
|
+
* already bound; write a `fixes_link` row for this entity pointing at
|
|
17937
|
+
* the same chat, return it.
|
|
17938
|
+
* c. Miss — create a fresh chat via the canonical `createChat` entrypoint
|
|
17939
|
+
* and write a `direct` mapping row.
|
|
17940
|
+
*
|
|
17941
|
+
* Concurrent webhook deliveries for a never-before-seen entity race on (c);
|
|
17942
|
+
* the composite primary key + ON CONFLICT DO NOTHING ensures only one row
|
|
17943
|
+
* survives. The losing caller falls back to a re-read so the chat stays
|
|
17944
|
+
* unique.
|
|
17945
|
+
*/
|
|
17946
|
+
async function resolveTargetChat(db, params) {
|
|
17947
|
+
const { organizationId, humanAgentId, delegateAgentId, entity, relatedEntities, eventType, action } = params;
|
|
17948
|
+
const direct = await lookupMapping(db, organizationId, humanAgentId, delegateAgentId, entity);
|
|
17949
|
+
if (direct) return {
|
|
17950
|
+
chatId: direct.chatId,
|
|
17951
|
+
created: false,
|
|
17952
|
+
boundVia: direct.boundVia
|
|
17953
|
+
};
|
|
17954
|
+
for (const ref of relatedEntities) {
|
|
17955
|
+
const linked = await lookupMapping(db, organizationId, humanAgentId, delegateAgentId, ref);
|
|
17956
|
+
if (!linked) continue;
|
|
17957
|
+
const inserted = await insertMappingIfAbsent(db, {
|
|
17958
|
+
organizationId,
|
|
17959
|
+
humanAgentId,
|
|
17960
|
+
delegateAgentId,
|
|
17961
|
+
entity,
|
|
17962
|
+
chatId: linked.chatId,
|
|
17963
|
+
boundVia: "fixes_link"
|
|
17964
|
+
});
|
|
17965
|
+
return {
|
|
17966
|
+
chatId: inserted.chatId,
|
|
17967
|
+
created: false,
|
|
17968
|
+
boundVia: inserted.boundVia
|
|
17969
|
+
};
|
|
17970
|
+
}
|
|
17971
|
+
const chat = await createEntityChat(db, humanAgentId, delegateAgentId, entity, eventType, action);
|
|
17972
|
+
const inserted = await insertMappingIfAbsent(db, {
|
|
17973
|
+
organizationId,
|
|
17974
|
+
humanAgentId,
|
|
17975
|
+
delegateAgentId,
|
|
17976
|
+
entity,
|
|
17977
|
+
chatId: chat.id,
|
|
17978
|
+
boundVia: "direct"
|
|
17979
|
+
});
|
|
17980
|
+
return {
|
|
17981
|
+
chatId: inserted.chatId,
|
|
17982
|
+
created: inserted.chatId === chat.id,
|
|
17983
|
+
boundVia: inserted.boundVia
|
|
17984
|
+
};
|
|
17985
|
+
}
|
|
17986
|
+
async function lookupMapping(db, organizationId, humanAgentId, delegateAgentId, entity) {
|
|
17987
|
+
const [row] = await db.select({
|
|
17988
|
+
chatId: githubEntityChatMappings.chatId,
|
|
17989
|
+
boundVia: githubEntityChatMappings.boundVia
|
|
17990
|
+
}).from(githubEntityChatMappings).where(and(eq(githubEntityChatMappings.organizationId, organizationId), eq(githubEntityChatMappings.humanAgentId, humanAgentId), eq(githubEntityChatMappings.delegateAgentId, delegateAgentId), eq(githubEntityChatMappings.entityType, entity.type), eq(githubEntityChatMappings.entityKey, entity.key))).limit(1);
|
|
17991
|
+
if (!row) return null;
|
|
17992
|
+
return {
|
|
17993
|
+
chatId: row.chatId,
|
|
17994
|
+
boundVia: row.boundVia === "fixes_link" ? "fixes_link" : "direct"
|
|
17995
|
+
};
|
|
17996
|
+
}
|
|
17997
|
+
async function insertMappingIfAbsent(db, params) {
|
|
17998
|
+
const [inserted] = await db.insert(githubEntityChatMappings).values({
|
|
17999
|
+
organizationId: params.organizationId,
|
|
18000
|
+
humanAgentId: params.humanAgentId,
|
|
18001
|
+
delegateAgentId: params.delegateAgentId,
|
|
18002
|
+
entityType: params.entity.type,
|
|
18003
|
+
entityKey: params.entity.key,
|
|
18004
|
+
chatId: params.chatId,
|
|
18005
|
+
boundVia: params.boundVia
|
|
18006
|
+
}).onConflictDoNothing({ target: [
|
|
18007
|
+
githubEntityChatMappings.organizationId,
|
|
18008
|
+
githubEntityChatMappings.humanAgentId,
|
|
18009
|
+
githubEntityChatMappings.delegateAgentId,
|
|
18010
|
+
githubEntityChatMappings.entityType,
|
|
18011
|
+
githubEntityChatMappings.entityKey
|
|
18012
|
+
] }).returning({
|
|
18013
|
+
chatId: githubEntityChatMappings.chatId,
|
|
18014
|
+
boundVia: githubEntityChatMappings.boundVia
|
|
18015
|
+
});
|
|
18016
|
+
if (inserted) return {
|
|
18017
|
+
chatId: inserted.chatId,
|
|
18018
|
+
boundVia: inserted.boundVia === "fixes_link" ? "fixes_link" : "direct"
|
|
18019
|
+
};
|
|
18020
|
+
const winner = await lookupMapping(db, params.organizationId, params.humanAgentId, params.delegateAgentId, params.entity);
|
|
18021
|
+
if (!winner) throw new Error("Unexpected: mapping insert conflicted but row not visible on re-read");
|
|
18022
|
+
return winner;
|
|
18023
|
+
}
|
|
18024
|
+
/**
|
|
18025
|
+
* Create a fresh chat for a (human, delegate, entity) tuple. Goes through the
|
|
18026
|
+
* canonical `createChat` so:
|
|
18027
|
+
* - cross-org participants are rejected (BadRequestError)
|
|
18028
|
+
* - direct agent-only chats automatically get `mode=mention_only`
|
|
18029
|
+
* - watcher rows are recomputed
|
|
18030
|
+
* - a future addParticipant call would upgrade the chat to `group` via
|
|
18031
|
+
* `maybeUpgradeDirectToGroup` instead of raw INSERT shortcuts
|
|
18032
|
+
*/
|
|
18033
|
+
async function createEntityChat(db, humanAgentId, delegateAgentId, entity, eventType, action) {
|
|
18034
|
+
const metadata = chatMetadataSchema$1.parse({
|
|
18035
|
+
source: "github",
|
|
18036
|
+
entityType: entity.type,
|
|
18037
|
+
entityKey: entity.key,
|
|
18038
|
+
...entity.url ? { entityUrl: entity.url } : {}
|
|
18039
|
+
});
|
|
18040
|
+
return { id: (await createChat(db, humanAgentId, {
|
|
18041
|
+
type: "direct",
|
|
18042
|
+
participantIds: [delegateAgentId],
|
|
18043
|
+
topic: formatEntityTitle(entity, eventType, action),
|
|
18044
|
+
metadata
|
|
18045
|
+
})).id };
|
|
18046
|
+
}
|
|
18047
|
+
const log$1 = createLogger$1("GithubWebhook");
|
|
18048
|
+
function verifySignature(secret, rawBody, signatureHeader) {
|
|
18049
|
+
const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
|
|
18050
|
+
const expectedBuf = Buffer.from(expected, "utf8");
|
|
18051
|
+
const receivedBuf = Buffer.from(signatureHeader, "utf8");
|
|
18052
|
+
if (expectedBuf.length !== receivedBuf.length || !timingSafeEqual(expectedBuf, receivedBuf)) throw new UnauthorizedError("Invalid webhook signature");
|
|
17671
18053
|
}
|
|
17672
18054
|
/** Extract unique @mentions from text. Returns lowercase usernames.
|
|
17673
18055
|
* Excludes email patterns (user@example.com) and team mentions (@org/team). */
|
|
@@ -17708,10 +18090,18 @@ function evaluateDelegateTarget(target, sourceOrgId) {
|
|
|
17708
18090
|
}
|
|
17709
18091
|
/**
|
|
17710
18092
|
* Route @mentions to delegate agents.
|
|
17711
|
-
*
|
|
17712
|
-
*
|
|
18093
|
+
*
|
|
18094
|
+
* For each mentioned GitHub user who maps to an agent with `delegate_mention`
|
|
18095
|
+
* configured, resolve which chat the event belongs to (via §4.4's
|
|
18096
|
+
* entity-clustering rules) and post a card from the human-bound agent to its
|
|
18097
|
+
* delegate.
|
|
18098
|
+
*
|
|
18099
|
+
* The entity argument is the §4.2 entity for the current event; `relatedRefs`
|
|
18100
|
+
* is the parsed `Fixes #N` list (empty for non-PR events). Both are
|
|
18101
|
+
* pre-computed by the caller so the heavy parsing doesn't run once per
|
|
18102
|
+
* mention.
|
|
17713
18103
|
*/
|
|
17714
|
-
async function routeMentionDelegations(app, organizationId, mentionedNames, ctx) {
|
|
18104
|
+
async function routeMentionDelegations(app, organizationId, mentionedNames, ctx, entity, relatedRefs) {
|
|
17715
18105
|
if (mentionedNames.length === 0) return 0;
|
|
17716
18106
|
const delegates = await app.db.select({
|
|
17717
18107
|
id: agents.uuid,
|
|
@@ -17740,8 +18130,24 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
|
|
|
17740
18130
|
continue;
|
|
17741
18131
|
}
|
|
17742
18132
|
try {
|
|
17743
|
-
const
|
|
17744
|
-
|
|
18133
|
+
const resolved = await resolveTargetChat(app.db, {
|
|
18134
|
+
organizationId,
|
|
18135
|
+
humanAgentId: agent.id,
|
|
18136
|
+
delegateAgentId: agent.delegateMention,
|
|
18137
|
+
entity,
|
|
18138
|
+
relatedEntities: relatedRefs,
|
|
18139
|
+
eventType: ctx.event,
|
|
18140
|
+
action: ctx.action ?? ""
|
|
18141
|
+
});
|
|
18142
|
+
log$1.info({
|
|
18143
|
+
chatId: resolved.chatId,
|
|
18144
|
+
entityType: entity.type,
|
|
18145
|
+
entityKey: entity.key,
|
|
18146
|
+
boundVia: resolved.boundVia,
|
|
18147
|
+
created: resolved.created,
|
|
18148
|
+
humanAgent: agent.name
|
|
18149
|
+
}, "resolved entity chat");
|
|
18150
|
+
const { message: msg, recipients } = await sendMessage(app.db, resolved.chatId, agent.id, {
|
|
17745
18151
|
format: "card",
|
|
17746
18152
|
content: {
|
|
17747
18153
|
type: "github_mention",
|
|
@@ -17752,13 +18158,20 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
|
|
|
17752
18158
|
sender: ctx.sender,
|
|
17753
18159
|
title: ctx.title,
|
|
17754
18160
|
body: ctx.body,
|
|
17755
|
-
url: ctx.url
|
|
18161
|
+
url: ctx.url,
|
|
18162
|
+
entity: {
|
|
18163
|
+
type: entity.type,
|
|
18164
|
+
key: entity.key,
|
|
18165
|
+
url: entity.url ?? null
|
|
18166
|
+
}
|
|
17756
18167
|
},
|
|
17757
18168
|
metadata: {
|
|
17758
18169
|
source: "github",
|
|
17759
18170
|
event: "mention_delegation",
|
|
17760
18171
|
mentionedUser: agent.name,
|
|
17761
|
-
action: ctx.action
|
|
18172
|
+
action: ctx.action,
|
|
18173
|
+
entityType: entity.type,
|
|
18174
|
+
entityKey: entity.key
|
|
17762
18175
|
}
|
|
17763
18176
|
});
|
|
17764
18177
|
notifyRecipients(app.notifier, recipients, msg.id);
|
|
@@ -17773,43 +18186,6 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
|
|
|
17773
18186
|
}
|
|
17774
18187
|
return routed;
|
|
17775
18188
|
}
|
|
17776
|
-
function parseIssuesPayload(body) {
|
|
17777
|
-
if (!isRecord(body)) throw new BadRequestError("Invalid payload: expected object");
|
|
17778
|
-
if (typeof body.action !== "string") throw new BadRequestError("Invalid payload: missing action");
|
|
17779
|
-
if (!isRecord(body.issue)) throw new BadRequestError("Invalid payload: missing issue");
|
|
17780
|
-
if (!isRecord(body.repository)) throw new BadRequestError("Invalid payload: missing repository");
|
|
17781
|
-
if (!isRecord(body.sender)) throw new BadRequestError("Invalid payload: missing sender");
|
|
17782
|
-
const issue = body.issue;
|
|
17783
|
-
const labels = Array.isArray(issue.labels) ? issue.labels.filter((l) => isRecord(l) && typeof l.name === "string") : [];
|
|
17784
|
-
return {
|
|
17785
|
-
action: body.action,
|
|
17786
|
-
issue: {
|
|
17787
|
-
number: typeof issue.number === "number" ? issue.number : 0,
|
|
17788
|
-
title: typeof issue.title === "string" ? issue.title : "",
|
|
17789
|
-
body: typeof issue.body === "string" ? issue.body : null,
|
|
17790
|
-
html_url: typeof issue.html_url === "string" ? issue.html_url : "",
|
|
17791
|
-
labels,
|
|
17792
|
-
state: typeof issue.state === "string" ? issue.state : "open"
|
|
17793
|
-
},
|
|
17794
|
-
repository: { full_name: typeof body.repository.full_name === "string" ? body.repository.full_name : "" },
|
|
17795
|
-
sender: { login: typeof body.sender.login === "string" ? body.sender.login : "" }
|
|
17796
|
-
};
|
|
17797
|
-
}
|
|
17798
|
-
function parseIssueCommentPayload(body) {
|
|
17799
|
-
const base = parseIssuesPayload(body);
|
|
17800
|
-
if (!isRecord(body)) throw new BadRequestError("Invalid payload: expected object");
|
|
17801
|
-
if (!isRecord(body.comment)) throw new BadRequestError("Invalid payload: missing comment");
|
|
17802
|
-
const comment = body.comment;
|
|
17803
|
-
const commentUser = isRecord(comment.user) ? comment.user : { login: "" };
|
|
17804
|
-
return {
|
|
17805
|
-
...base,
|
|
17806
|
-
comment: {
|
|
17807
|
-
body: typeof comment.body === "string" ? comment.body : "",
|
|
17808
|
-
html_url: typeof comment.html_url === "string" ? comment.html_url : "",
|
|
17809
|
-
user: { login: typeof commentUser.login === "string" ? commentUser.login : "" }
|
|
17810
|
-
}
|
|
17811
|
-
};
|
|
17812
|
-
}
|
|
17813
18189
|
async function githubWebhookRoutes(app) {
|
|
17814
18190
|
app.addContentTypeParser("application/json", { parseAs: "buffer" }, (_request, body, done) => {
|
|
17815
18191
|
done(null, body);
|
|
@@ -17839,6 +18215,11 @@ async function githubWebhookRoutes(app) {
|
|
|
17839
18215
|
ok: true,
|
|
17840
18216
|
event: "ping"
|
|
17841
18217
|
});
|
|
18218
|
+
if (shouldSilent(eventType, payload)) return reply.status(200).send({
|
|
18219
|
+
ok: true,
|
|
18220
|
+
event: eventType,
|
|
18221
|
+
silent: true
|
|
18222
|
+
});
|
|
17842
18223
|
const deliveryHeader = request.headers["x-github-delivery"];
|
|
17843
18224
|
const deliveryId = typeof deliveryHeader === "string" && deliveryHeader.length > 0 ? deliveryHeader : null;
|
|
17844
18225
|
if (deliveryId) {
|
|
@@ -17855,16 +18236,17 @@ async function githubWebhookRoutes(app) {
|
|
|
17855
18236
|
}
|
|
17856
18237
|
}
|
|
17857
18238
|
try {
|
|
17858
|
-
if (eventType === "issues") return await handleIssuesEvent(app, orgId, eventType, payload, reply);
|
|
17859
|
-
if (eventType === "issue_comment") return await handleIssueCommentEvent(app, orgId, eventType, payload, reply);
|
|
17860
|
-
let mentionsRouted = 0;
|
|
17861
|
-
const allowedActions = MENTION_ACTIONS[eventType];
|
|
17862
18239
|
const action = isRecord(payload) && typeof payload.action === "string" ? payload.action : void 0;
|
|
17863
|
-
|
|
18240
|
+
const allowedActions = MENTION_ACTIONS[eventType];
|
|
18241
|
+
if (!allowedActions || !action || !allowedActions.includes(action)) return reply.status(200).send({
|
|
18242
|
+
ok: true,
|
|
18243
|
+
event: eventType,
|
|
18244
|
+
handled: false
|
|
18245
|
+
});
|
|
18246
|
+
const mentionsRouted = await handleMentionDelegation(app, orgId, eventType, payload);
|
|
17864
18247
|
return reply.status(200).send({
|
|
17865
18248
|
ok: true,
|
|
17866
18249
|
event: eventType,
|
|
17867
|
-
handled: mentionsRouted > 0,
|
|
17868
18250
|
mentionsRouted
|
|
17869
18251
|
});
|
|
17870
18252
|
} catch (err) {
|
|
@@ -18036,9 +18418,15 @@ async function handleMentionDelegation(app, organizationId, eventType, payload)
|
|
|
18036
18418
|
const textMentions = extractMentions$1(extractEventText(eventType, payload));
|
|
18037
18419
|
const structuralMentions = extractStructuralMentions(eventType, payload);
|
|
18038
18420
|
const mentions = [...new Set([...textMentions, ...structuralMentions])];
|
|
18039
|
-
|
|
18040
|
-
|
|
18041
|
-
return 0;
|
|
18421
|
+
if (mentions.length === 0) return 0;
|
|
18422
|
+
const ctx = extractEventContext(eventType, payload);
|
|
18423
|
+
if (!ctx) return 0;
|
|
18424
|
+
const entity = extractEventEntity(eventType, payload);
|
|
18425
|
+
if (!entity) {
|
|
18426
|
+
log$1.warn({ eventType }, "mention extracted but no entity resolvable; skipping fan-out");
|
|
18427
|
+
return 0;
|
|
18428
|
+
}
|
|
18429
|
+
return routeMentionDelegations(app, organizationId, mentions, ctx, entity, eventType === "pull_request" && ctx.repository.length > 0 ? parseFixesRefs(ctx.body, ctx.repository) : []);
|
|
18042
18430
|
}
|
|
18043
18431
|
/** Actions that represent new/changed content (worth scanning for @mentions).
|
|
18044
18432
|
* Note: `pull_request.review_requested` doesn't carry an @mention in any
|
|
@@ -18059,124 +18447,6 @@ const MENTION_ACTIONS = {
|
|
|
18059
18447
|
discussion_comment: ["created"],
|
|
18060
18448
|
commit_comment: ["created"]
|
|
18061
18449
|
};
|
|
18062
|
-
async function handleIssuesEvent(app, organizationId, eventType, payload, reply) {
|
|
18063
|
-
const data = parseIssuesPayload(payload);
|
|
18064
|
-
if (MENTION_ACTIONS.issues?.includes(data.action)) await handleMentionDelegation(app, organizationId, eventType, payload);
|
|
18065
|
-
if (![
|
|
18066
|
-
"opened",
|
|
18067
|
-
"edited",
|
|
18068
|
-
"labeled"
|
|
18069
|
-
].includes(data.action)) return reply.status(200).send({
|
|
18070
|
-
ok: true,
|
|
18071
|
-
event: "issues",
|
|
18072
|
-
action: data.action,
|
|
18073
|
-
handled: false
|
|
18074
|
-
});
|
|
18075
|
-
const [senderId, targetAgentId] = await Promise.all([ensureGitHubAdapterAgent(app.db, organizationId), findTargetAgent(app.db, organizationId, data.repository.full_name)]);
|
|
18076
|
-
if (!targetAgentId) {
|
|
18077
|
-
log$1.warn({
|
|
18078
|
-
repo: data.repository.full_name,
|
|
18079
|
-
event: "issue"
|
|
18080
|
-
}, "no target agent found for GitHub event");
|
|
18081
|
-
return reply.status(200).send({
|
|
18082
|
-
ok: true,
|
|
18083
|
-
event: "issues",
|
|
18084
|
-
action: data.action,
|
|
18085
|
-
routed: false
|
|
18086
|
-
});
|
|
18087
|
-
}
|
|
18088
|
-
const content = {
|
|
18089
|
-
type: "github_issue",
|
|
18090
|
-
action: data.action,
|
|
18091
|
-
issue: {
|
|
18092
|
-
number: data.issue.number,
|
|
18093
|
-
title: data.issue.title,
|
|
18094
|
-
body: data.issue.body,
|
|
18095
|
-
url: data.issue.html_url,
|
|
18096
|
-
labels: data.issue.labels.map((l) => l.name),
|
|
18097
|
-
state: data.issue.state
|
|
18098
|
-
},
|
|
18099
|
-
repository: data.repository.full_name,
|
|
18100
|
-
sender: data.sender.login
|
|
18101
|
-
};
|
|
18102
|
-
const metadata = {
|
|
18103
|
-
source: "github",
|
|
18104
|
-
event: "issues",
|
|
18105
|
-
action: data.action
|
|
18106
|
-
};
|
|
18107
|
-
const chat = await findOrCreateDirectChat(app.db, senderId, targetAgentId);
|
|
18108
|
-
const { message: msg, recipients } = await sendMessage(app.db, chat.id, senderId, {
|
|
18109
|
-
format: "card",
|
|
18110
|
-
content,
|
|
18111
|
-
metadata
|
|
18112
|
-
});
|
|
18113
|
-
notifyRecipients(app.notifier, recipients, msg.id);
|
|
18114
|
-
return reply.status(200).send({
|
|
18115
|
-
ok: true,
|
|
18116
|
-
event: "issues",
|
|
18117
|
-
action: data.action,
|
|
18118
|
-
routed: true
|
|
18119
|
-
});
|
|
18120
|
-
}
|
|
18121
|
-
async function handleIssueCommentEvent(app, organizationId, eventType, payload, reply) {
|
|
18122
|
-
const data = parseIssueCommentPayload(payload);
|
|
18123
|
-
if (MENTION_ACTIONS.issue_comment?.includes(data.action)) await handleMentionDelegation(app, organizationId, eventType, payload);
|
|
18124
|
-
if (data.action !== "created") return reply.status(200).send({
|
|
18125
|
-
ok: true,
|
|
18126
|
-
event: "issue_comment",
|
|
18127
|
-
action: data.action,
|
|
18128
|
-
handled: false
|
|
18129
|
-
});
|
|
18130
|
-
const [senderId, targetAgentId] = await Promise.all([ensureGitHubAdapterAgent(app.db, organizationId), findTargetAgent(app.db, organizationId, data.repository.full_name)]);
|
|
18131
|
-
if (!targetAgentId) {
|
|
18132
|
-
log$1.warn({
|
|
18133
|
-
repo: data.repository.full_name,
|
|
18134
|
-
event: "issue_comment"
|
|
18135
|
-
}, "no target agent found for GitHub event");
|
|
18136
|
-
return reply.status(200).send({
|
|
18137
|
-
ok: true,
|
|
18138
|
-
event: "issue_comment",
|
|
18139
|
-
action: data.action,
|
|
18140
|
-
routed: false
|
|
18141
|
-
});
|
|
18142
|
-
}
|
|
18143
|
-
const content = {
|
|
18144
|
-
type: "github_issue_comment",
|
|
18145
|
-
action: data.action,
|
|
18146
|
-
issue: {
|
|
18147
|
-
number: data.issue.number,
|
|
18148
|
-
title: data.issue.title,
|
|
18149
|
-
url: data.issue.html_url,
|
|
18150
|
-
labels: data.issue.labels.map((l) => l.name),
|
|
18151
|
-
state: data.issue.state
|
|
18152
|
-
},
|
|
18153
|
-
comment: {
|
|
18154
|
-
body: data.comment.body,
|
|
18155
|
-
url: data.comment.html_url,
|
|
18156
|
-
author: data.comment.user.login
|
|
18157
|
-
},
|
|
18158
|
-
repository: data.repository.full_name,
|
|
18159
|
-
sender: data.sender.login
|
|
18160
|
-
};
|
|
18161
|
-
const metadata = {
|
|
18162
|
-
source: "github",
|
|
18163
|
-
event: "issue_comment",
|
|
18164
|
-
action: data.action
|
|
18165
|
-
};
|
|
18166
|
-
const chat = await findOrCreateDirectChat(app.db, senderId, targetAgentId);
|
|
18167
|
-
const { message: msg, recipients } = await sendMessage(app.db, chat.id, senderId, {
|
|
18168
|
-
format: "card",
|
|
18169
|
-
content,
|
|
18170
|
-
metadata
|
|
18171
|
-
});
|
|
18172
|
-
notifyRecipients(app.notifier, recipients, msg.id);
|
|
18173
|
-
return reply.status(200).send({
|
|
18174
|
-
ok: true,
|
|
18175
|
-
event: "issue_comment",
|
|
18176
|
-
action: data.action,
|
|
18177
|
-
routed: true
|
|
18178
|
-
});
|
|
18179
|
-
}
|
|
18180
18450
|
var schema_exports = /* @__PURE__ */ __exportAll({
|
|
18181
18451
|
adapterAgentMappings: () => adapterAgentMappings,
|
|
18182
18452
|
adapterChatMappings: () => adapterChatMappings,
|
|
@@ -18191,6 +18461,7 @@ var schema_exports = /* @__PURE__ */ __exportAll({
|
|
|
18191
18461
|
chatSubscriptions: () => chatSubscriptions,
|
|
18192
18462
|
chats: () => chats,
|
|
18193
18463
|
clients: () => clients,
|
|
18464
|
+
githubEntityChatMappings: () => githubEntityChatMappings,
|
|
18194
18465
|
inboxEntries: () => inboxEntries,
|
|
18195
18466
|
invitationRedemptions: () => invitationRedemptions,
|
|
18196
18467
|
invitations: () => invitations,
|
|
@@ -20330,4 +20601,4 @@ function registerSaaSConnectCommand(program) {
|
|
|
20330
20601
|
});
|
|
20331
20602
|
}
|
|
20332
20603
|
//#endregion
|
|
20333
|
-
export { formatStaleReason as $, checkDocker as A, isServiceSupported as B, createApiNameResolver as C, checkBackgroundService as D, checkAgentConfigs as E, checkWebSocket as F, uninstallClientService as G, restartClientService as H, printResults as I, stopPostgres as J, ensurePostgres as K, reconcileAgentConfigs as L, checkServerConfig as M, checkServerHealth as N, checkClientConfig as O, checkServerReachable as P, findStaleAliases as Q, getClientServiceStatus as R, runHomeMigration as S, runMigrations as T, startClientService as U, resolveCliInvocation as V, stopClientService as W, handleClientOrgMismatch as X, ClientRuntime as Y, rotateClientIdWithBackup as Z, formatCheckReport as _, declineUpdate as a,
|
|
20604
|
+
export { formatStaleReason as $, checkDocker as A, isServiceSupported as B, createApiNameResolver as C, checkBackgroundService as D, checkAgentConfigs as E, checkWebSocket as F, uninstallClientService as G, restartClientService as H, printResults as I, stopPostgres as J, ensurePostgres as K, reconcileAgentConfigs as L, checkServerConfig as M, checkServerHealth as N, checkClientConfig as O, checkServerReachable as P, findStaleAliases as Q, getClientServiceStatus as R, runHomeMigration as S, runMigrations as T, startClientService as U, resolveCliInvocation as V, stopClientService as W, handleClientOrgMismatch as X, ClientRuntime as Y, rotateClientIdWithBackup as Z, formatCheckReport as _, declineUpdate as a, fail as at, onboardCreate as b, detectInstallMode as c, ClientUserMismatchError as ct, startServer as d, SessionRegistry as dt, removeLocalAgent as et, reconcileLocalRuntimeProviders as f, cleanWorkspaces as ft, promptMissingFields as g, promptAddAgent as h, configureClientLoggerForService as ht, createExecuteUpdate as i, resolveSenderName as it, checkNodeVersion as j, checkDatabase as k, fetchLatestVersion as l, FirstTreeHubSDK as lt, isInteractive as m, applyClientLoggerConfig as mt, deriveHubUrlFromToken as n, hasUser as nt, promptUpdate as o, success as ot, uploadClientCapabilities as p, probeCapabilities as pt, isDockerAvailable as q, registerSaaSConnectCommand as r, resolveReplyToFromEnv as rt, PACKAGE_NAME as s, ClientOrgMismatchError as st, HubUrlDerivationError as t, createOwner as tt, installGlobalLatest as u, SdkError as ut, loadOnboardState as v, migrateLocalAgentDirs as w, saveOnboardState as x, onboardCheck as y, installClientService as z };
|