@agent-team-foundation/first-tree-hub 0.13.0 → 0.14.1
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-Cya2OoHz.mjs → bootstrap-C15ZBOCC.mjs} +1 -0
- package/dist/cli/index.mjs +7 -7
- package/dist/{client-h4KZ3b9o-CQyibXig.mjs → client-CREn8bJ0-C5fHJir6.mjs} +2 -2
- package/dist/{client-BH4CmUL0-CybE3kuP.mjs → client-CzXmweS9-DhUiuQvL.mjs} +59 -6
- package/dist/{dist-C8yStx2L.mjs → dist-1XGLJMOq.mjs} +109 -9
- package/dist/drizzle/0044_agent_avatar_color.sql +11 -0
- package/dist/drizzle/0045_agent_avatar_image.sql +17 -0
- package/dist/drizzle/meta/_journal.json +14 -0
- package/dist/{feishu-D_vnqC6a.mjs → feishu-BGx71p5s.mjs} +1 -1
- package/dist/index.mjs +5 -5
- package/dist/{invitation-CNv7gfFF-D93KQte0.mjs → invitation-CNv7gfFF-DOFZ75wb.mjs} +1 -1
- package/dist/{saas-connect-Bb5LR4y6.mjs → saas-connect-BBRxjmBS.mjs} +635 -113
- package/dist/web/assets/index-BOK7e_td.css +1 -0
- package/dist/web/assets/{index-CJcRUZ8l.js → index-DMqnX4IR.js} +1 -1
- package/dist/web/assets/index-QDcpYpEa.js +416 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-DL_9NFkt.js +0 -421
- package/dist/web/assets/index-DaWEZnjh.css +0 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { a as __toCommonJS, o as __toESM, t as __commonJSMin } from "./chunk-BSw8zbkd.mjs";
|
|
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
|
-
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-
|
|
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-C15ZBOCC.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 listMeChatSourceCountsQuerySchema, A as createMeChatSchema, At as updateOrganizationSchema, B as githubAppInstallationClaimBodySchema, C as clientRegisterSchema, Ct as submitQuestionAnswerSchema, D as createAdapterMappingSchema, Dt as updateChatSchema, E as createAdapterConfigSchema, Et as updateAgentSchema, F as delegateFeishuUserSchema, G as imageInlineContentSchema, H as githubCallbackQuerySchema, I as dryRunAgentRuntimeConfigSchema, J as inboxPollQuerySchema, K as inboxAckFrameSchema, M as createOrgFromMeSchema, O as createAgentSchema, Ot as updateClientCapabilitiesSchema, P as defaultRuntimeConfigPayload, Q as joinByInvitationSchema, R as getMeDocResponseSchema, St as stripCode, T as contextTreeSnapshotSchema, Tt as updateAgentRuntimeConfigSchema, U as githubDevCallbackQuerySchema, V as githubAppInstallationPermissionsSchema$1, W as githubStartQuerySchema, X as isRedactedEnvValue, Y as isOrgSettingNamespace, Z as isReservedAgentName$1, _ as agentBindRequestSchema, _t as sendToAgentSchema, a as AGENT_TYPES, at as paginationQuerySchema, b as agentTypeSchema$1, bt as sessionReconcileRequestSchema, d as MENTION_REGEX, dt as refreshTokenSchema, et as listMeChatsQuerySchema, f as NOTIFICATION_TYPES, ft as runtimeStateMessageSchema, g as addParticipantSchema, gt as sendMessageSchema, h as addMeChatParticipantsSchema, ht as selfServiceFeishuBotSchema, i as AGENT_STATUSES, it as onboardingEventSchema, j as createMemberSchema, jt as wsAuthFrameSchema, k as createChatSchema, kt as updateMemberSchema, l as GITHUB_ENTITY_TYPES, m as WS_AUTH_FRAME_TIMEOUT_MS, n as AGENT_NAME_REGEX$1, nt as messageSourceSchema$1, o as AGENT_VISIBILITY, ot as patchChatEngagementSchema, p as ORG_SETTINGS_NAMESPACES$1, pt as safeRedirectPath, q as inboxDeliverFrameSchema$1, r as AGENT_SELECTOR_HEADER$1, rt as notificationQuerySchema, s as CHAT_ENGAGEMENT_STATUSES, st as patchOnboardingSchema, t as AGENT_BIND_REJECT_REASONS, tt as loginSchema, ut as rebindAgentSchema, v as agentPinnedMessageSchema$1, vt as sessionEventMessageSchema, w as connectTokenExchangeSchema, wt as updateAdapterConfigSchema, x as chatMetadataSchema$1, xt as sessionStateMessageSchema, y as agentRuntimeConfigPayloadSchema$1, yt as sessionEventSchema$1, z as getMeDocSchema } from "./dist-1XGLJMOq.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-LPcARA4K-Dbrptiyz.mjs";
|
|
7
7
|
import { n as init_esm, r as trace, t as esm_exports } from "./esm-iadMkGbV.mjs";
|
|
8
|
-
import { $ as notifyRecipients, A as getPresence, B as listAgentsManagedByUser, C as ensureParticipant, D as getChatDetail, E as getCachedAudience, F as joinAsParticipant, G as listClients, H as listChatParticipantsWithNames, I as joinChat, K as listClientsForOrgAdmin, L as leaveAsParticipant, M as heartbeatInstance, N as inboxEntries, O as getClient, P as invalidateChatAudience, Q as messages, R as leaveChat, S as ensureCanJoin, T as getActivityOverview, U as listChats, V as listAgentsWithRuntime, W as listChatsForMember, X as markSupersededByChat, Y as markStaleAgents, Z as members, _ as createChat, _t as unbindAgent, a as agentVisibilityCondition, at as registerClient, b as disconnectClient, c as assertParticipant, ct as resolveChatMembership, d as chatMembership, dt as sendToAgent$1, et as pendingQuestions, f as chats, ft as serverInstances, g as clients, gt as touchAgent, h as cleanupStalePresence, ht as submitAnswer, i as agentPresence, it as registerChatMessageDispatcher, j as heartbeatClient, k as getOnlineCount, l as bindAgent, lt as retireClient, m as cleanupStaleClients, mt as setRuntimeState, n as addParticipant, nt as recomputeWatchersForAgent, o as agents, ot as removeParticipant, p as claimClient, pt as setOffline, q as listMessages, r as agentChatSessions, rt as recomputeWatchersForMember, s as assertClientOwner, st as resetActivity, t as addChatParticipants, tt as recomputeChatWatchers, u as changeChatType, ut as sendMessage, v as createNotifier, vt as updateClientCapabilities, w as findOrCreateDirectChat, x as editMessage, y as deriveAuthState, yt as upsertSessionState, z as listActiveAgentsPinnedToClient } from "./client-
|
|
8
|
+
import { $ as notifyRecipients, A as getPresence, B as listAgentsManagedByUser, C as ensureParticipant, D as getChatDetail, E as getCachedAudience, F as joinAsParticipant, G as listClients, H as listChatParticipantsWithNames, I as joinChat, K as listClientsForOrgAdmin, L as leaveAsParticipant, M as heartbeatInstance, N as inboxEntries, O as getClient, P as invalidateChatAudience, Q as messages, R as leaveChat, S as ensureCanJoin, T as getActivityOverview, U as listChats, V as listAgentsWithRuntime, W as listChatsForMember, X as markSupersededByChat, Y as markStaleAgents, Z as members, _ as createChat, _t as unbindAgent, a as agentVisibilityCondition, at as registerClient, b as disconnectClient, c as assertParticipant, ct as resolveChatMembership, d as chatMembership, dt as sendToAgent$1, et as pendingQuestions, f as chats, ft as serverInstances, g as clients, gt as touchAgent, h as cleanupStalePresence, ht as submitAnswer, i as agentPresence, it as registerChatMessageDispatcher, j as heartbeatClient, k as getOnlineCount, l as bindAgent, lt as retireClient, m as cleanupStaleClients, mt as setRuntimeState, n as addParticipant, nt as recomputeWatchersForAgent, o as agents, ot as removeParticipant, p as claimClient, pt as setOffline, q as listMessages, r as agentChatSessions, rt as recomputeWatchersForMember, s as assertClientOwner, st as resetActivity, t as addChatParticipants, tt as recomputeChatWatchers, u as changeChatType, ut as sendMessage, v as createNotifier, vt as updateClientCapabilities, w as findOrCreateDirectChat, x as editMessage, y as deriveAuthState, yt as upsertSessionState, z as listActiveAgentsPinnedToClient } from "./client-CzXmweS9-DhUiuQvL.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-DZO4NX3P-BPxTeHf-.mjs";
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
import { ZodError, z } from "zod";
|
|
@@ -16,7 +16,7 @@ import { EventEmitter } from "node:events";
|
|
|
16
16
|
import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync, writeSync } from "node:fs";
|
|
17
17
|
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
18
18
|
import WebSocket from "ws";
|
|
19
|
-
import { chmod, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
19
|
+
import { chmod, mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
20
20
|
import { parse, stringify } from "yaml";
|
|
21
21
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
22
22
|
import { execFile, execFileSync, execSync, spawn, spawnSync } from "node:child_process";
|
|
@@ -659,6 +659,16 @@ const agentTypeSchema = z.enum([
|
|
|
659
659
|
"autonomous_agent"
|
|
660
660
|
]);
|
|
661
661
|
const agentVisibilitySchema = z.enum(["private", "organization"]);
|
|
662
|
+
const avatarColorTokenSchema = z.enum([
|
|
663
|
+
"hue-0",
|
|
664
|
+
"hue-1",
|
|
665
|
+
"hue-2",
|
|
666
|
+
"hue-3",
|
|
667
|
+
"hue-4",
|
|
668
|
+
"hue-5",
|
|
669
|
+
"hue-6",
|
|
670
|
+
"hue-7"
|
|
671
|
+
]);
|
|
662
672
|
const agentSourceSchema = z.enum(["admin-api", "portal"]);
|
|
663
673
|
z.enum(["active", "suspended"]);
|
|
664
674
|
/**
|
|
@@ -705,7 +715,8 @@ z.object({
|
|
|
705
715
|
visibility: agentVisibilitySchema.optional(),
|
|
706
716
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
707
717
|
managerId: z.string().nullable().optional(),
|
|
708
|
-
clientId: z.string().min(1).max(100).nullable().optional()
|
|
718
|
+
clientId: z.string().min(1).max(100).nullable().optional(),
|
|
719
|
+
avatarColorToken: avatarColorTokenSchema.nullable().optional()
|
|
709
720
|
});
|
|
710
721
|
z.object({
|
|
711
722
|
clientId: z.string().min(1).max(100),
|
|
@@ -727,6 +738,8 @@ z.object({
|
|
|
727
738
|
managerId: z.string().nullable(),
|
|
728
739
|
clientId: z.string().nullable(),
|
|
729
740
|
runtimeProvider: runtimeProviderSchema,
|
|
741
|
+
avatarColorToken: z.string().nullable(),
|
|
742
|
+
avatarImageUrl: z.string().nullable(),
|
|
730
743
|
presenceStatus: presenceStatusSchema.optional(),
|
|
731
744
|
createdAt: z.string(),
|
|
732
745
|
updatedAt: z.string()
|
|
@@ -788,6 +801,14 @@ const chatMetadataSchema = z.discriminatedUnion("source", [githubChatMetadataSch
|
|
|
788
801
|
* sneak through `{ source: "github" }` without the required fields.
|
|
789
802
|
*/
|
|
790
803
|
const optionalChatMetadataSchema = z.union([z.object({}).strict(), chatMetadataSchema]);
|
|
804
|
+
const chatSourceSchema = z.enum([
|
|
805
|
+
"manual",
|
|
806
|
+
"github_issue",
|
|
807
|
+
"github_pull_request",
|
|
808
|
+
"github_discussion",
|
|
809
|
+
"github_commit",
|
|
810
|
+
"feishu"
|
|
811
|
+
]);
|
|
791
812
|
const chatTypeSchema = z.enum(["direct", "group"]);
|
|
792
813
|
const chatEngagementStatusSchema = z.enum([
|
|
793
814
|
"active",
|
|
@@ -999,6 +1020,11 @@ const contextTreeSummarySchema = z.object({
|
|
|
999
1020
|
removedCount: z.number().int().nonnegative(),
|
|
1000
1021
|
changedNodeCount: z.number().int().nonnegative()
|
|
1001
1022
|
});
|
|
1023
|
+
const contextTreeUsageSummarySchema = z.object({
|
|
1024
|
+
windowDays: z.number().int().positive(),
|
|
1025
|
+
agentCount: z.number().int().nonnegative(),
|
|
1026
|
+
usageCount: z.number().int().nonnegative()
|
|
1027
|
+
});
|
|
1002
1028
|
z.object({
|
|
1003
1029
|
repo: z.string().nullable(),
|
|
1004
1030
|
branch: z.string().nullable(),
|
|
@@ -1007,6 +1033,7 @@ z.object({
|
|
|
1007
1033
|
snapshotStatus: contextTreeSnapshotStatusSchema,
|
|
1008
1034
|
contextStatus: contextTreeStatusSchema,
|
|
1009
1035
|
summary: contextTreeSummarySchema,
|
|
1036
|
+
usage: contextTreeUsageSummarySchema,
|
|
1010
1037
|
updates: z.array(contextTreeUpdateSchema),
|
|
1011
1038
|
nodes: z.array(contextTreeNodeSchema),
|
|
1012
1039
|
edges: z.array(contextTreeEdgeSchema),
|
|
@@ -1283,12 +1310,15 @@ z.object({
|
|
|
1283
1310
|
cursor: z.string().optional(),
|
|
1284
1311
|
limit: z.coerce.number().int().min(1).max(200).default(50),
|
|
1285
1312
|
filter: meChatFilterSchema.default("all"),
|
|
1286
|
-
engagement: chatEngagementViewSchema.default("active")
|
|
1313
|
+
engagement: chatEngagementViewSchema.default("active"),
|
|
1314
|
+
source: chatSourceSchema.optional()
|
|
1287
1315
|
});
|
|
1288
1316
|
const meChatParticipantSchema = z.object({
|
|
1289
1317
|
agentId: z.string(),
|
|
1290
1318
|
displayName: z.string(),
|
|
1291
|
-
type: z.string()
|
|
1319
|
+
type: z.string(),
|
|
1320
|
+
avatarColorToken: z.string().nullable(),
|
|
1321
|
+
avatarImageUrl: z.string().nullable()
|
|
1292
1322
|
});
|
|
1293
1323
|
/**
|
|
1294
1324
|
* Live activity hint surfaced in the conversation row's time slot. Derived
|
|
@@ -1348,6 +1378,47 @@ z.object({
|
|
|
1348
1378
|
type: z.literal("chat:message"),
|
|
1349
1379
|
chatId: z.string()
|
|
1350
1380
|
});
|
|
1381
|
+
/**
|
|
1382
|
+
* Per-source aggregate for the conversation-list tag bar.
|
|
1383
|
+
*
|
|
1384
|
+
* - `chatCount` — number of chats the caller is in for this source. Used
|
|
1385
|
+
* to hide tags whose count is 0 ("don't render a PR tag if there are no
|
|
1386
|
+
* PRs").
|
|
1387
|
+
* - `unreadChatCount` — number of chats whose `unread_mention_count > 0`.
|
|
1388
|
+
* This is "chats with unread mentions", NOT "total mention count", so
|
|
1389
|
+
* the badge on each tag matches the semantics of the existing `unread`
|
|
1390
|
+
* filter pill (`totalUnread` in `pages/workspace/conversations`) — a
|
|
1391
|
+
* `2` on the PR tag means "2 PR chats have unread mentions", which is
|
|
1392
|
+
* what a user expects to click into.
|
|
1393
|
+
*
|
|
1394
|
+
* The map ALWAYS contains the `manual` key (the default tab is always
|
|
1395
|
+
* available, even at zero counts); other keys are present only when the
|
|
1396
|
+
* caller has at least one chat for that source.
|
|
1397
|
+
*/
|
|
1398
|
+
const chatSourceCountSchema = z.object({
|
|
1399
|
+
chatCount: z.number().int().nonnegative(),
|
|
1400
|
+
unreadChatCount: z.number().int().nonnegative()
|
|
1401
|
+
});
|
|
1402
|
+
z.object({ engagement: chatEngagementViewSchema.default("active") });
|
|
1403
|
+
z.object({ counts: z.partialRecord(chatSourceSchema, chatSourceCountSchema) });
|
|
1404
|
+
const workspaceDocRefSchema = z.object({
|
|
1405
|
+
type: z.literal("workspace"),
|
|
1406
|
+
chatId: z.string().trim().min(1),
|
|
1407
|
+
agentId: z.string().trim().min(1),
|
|
1408
|
+
basePath: z.string().trim().optional(),
|
|
1409
|
+
path: z.string().trim().min(1)
|
|
1410
|
+
});
|
|
1411
|
+
const documentContextSchema = z.object({ basePath: z.string().trim().min(1) });
|
|
1412
|
+
z.object({
|
|
1413
|
+
agentId: z.string().trim().min(1),
|
|
1414
|
+
basePath: z.string().trim().optional(),
|
|
1415
|
+
path: z.string().trim().min(1)
|
|
1416
|
+
});
|
|
1417
|
+
z.object({
|
|
1418
|
+
ref: workspaceDocRefSchema,
|
|
1419
|
+
path: z.string(),
|
|
1420
|
+
content: z.string()
|
|
1421
|
+
});
|
|
1351
1422
|
z.enum([
|
|
1352
1423
|
"connect",
|
|
1353
1424
|
"create_agent",
|
|
@@ -1399,7 +1470,8 @@ z.object({
|
|
|
1399
1470
|
organizationId: z.string(),
|
|
1400
1471
|
organizationName: z.string(),
|
|
1401
1472
|
role: z.enum(["admin", "member"]),
|
|
1402
|
-
agentId: z.string()
|
|
1473
|
+
agentId: z.string(),
|
|
1474
|
+
orgHasOtherMembers: z.boolean()
|
|
1403
1475
|
});
|
|
1404
1476
|
const memberRoleSchema = z.enum(["admin", "member"]);
|
|
1405
1477
|
const memberSchema = z.object({
|
|
@@ -1774,7 +1846,8 @@ const sessionEventKind = z.enum([
|
|
|
1774
1846
|
"error",
|
|
1775
1847
|
"assistant_text",
|
|
1776
1848
|
"thinking",
|
|
1777
|
-
"turn_end"
|
|
1849
|
+
"turn_end",
|
|
1850
|
+
"context_tree_usage"
|
|
1778
1851
|
]);
|
|
1779
1852
|
const toolCallEventPayload = z.object({
|
|
1780
1853
|
toolUseId: z.string(),
|
|
@@ -1816,6 +1889,10 @@ const thinkingEventPayload = z.object({});
|
|
|
1816
1889
|
* completed turns to show only the final result message.
|
|
1817
1890
|
*/
|
|
1818
1891
|
const turnEndEventPayload = z.object({ status: z.enum(["success", "error"]) });
|
|
1892
|
+
const contextTreeUsageEventPayload = z.object({
|
|
1893
|
+
purpose: z.literal("design_decision"),
|
|
1894
|
+
treeRepoUrl: z.string().nullable()
|
|
1895
|
+
});
|
|
1819
1896
|
const sessionEventSchema = z.discriminatedUnion("kind", [
|
|
1820
1897
|
z.object({
|
|
1821
1898
|
kind: z.literal("tool_call"),
|
|
@@ -1836,6 +1913,10 @@ const sessionEventSchema = z.discriminatedUnion("kind", [
|
|
|
1836
1913
|
z.object({
|
|
1837
1914
|
kind: z.literal("turn_end"),
|
|
1838
1915
|
payload: turnEndEventPayload
|
|
1916
|
+
}),
|
|
1917
|
+
z.object({
|
|
1918
|
+
kind: z.literal("context_tree_usage"),
|
|
1919
|
+
payload: contextTreeUsageEventPayload
|
|
1839
1920
|
})
|
|
1840
1921
|
]);
|
|
1841
1922
|
z.object({
|
|
@@ -1849,7 +1930,8 @@ z.object({
|
|
|
1849
1930
|
errorEventPayload,
|
|
1850
1931
|
assistantTextEventPayload,
|
|
1851
1932
|
thinkingEventPayload,
|
|
1852
|
-
turnEndEventPayload
|
|
1933
|
+
turnEndEventPayload,
|
|
1934
|
+
contextTreeUsageEventPayload
|
|
1853
1935
|
]),
|
|
1854
1936
|
createdAt: z.string()
|
|
1855
1937
|
});
|
|
@@ -2018,6 +2100,7 @@ defineConfig({
|
|
|
2018
2100
|
host: field(z.string().default("127.0.0.1"), { env: "FIRST_TREE_HUB_HOST" }),
|
|
2019
2101
|
publicUrl: field(z.string().optional(), { env: "FIRST_TREE_HUB_PUBLIC_URL" })
|
|
2020
2102
|
},
|
|
2103
|
+
workspace: { root: field(z.string().default(join(DEFAULT_DATA_DIR, "workspaces")), { env: "FIRST_TREE_HUB_WORKSPACES_ROOT" }) },
|
|
2021
2104
|
secrets: {
|
|
2022
2105
|
jwtSecret: field(z.string(), {
|
|
2023
2106
|
env: "FIRST_TREE_HUB_JWT_SECRET",
|
|
@@ -3353,68 +3436,44 @@ function installFirstTreeIntegration(options) {
|
|
|
3353
3436
|
function generateToolsDoc() {
|
|
3354
3437
|
return `# Agent Hub SDK
|
|
3355
3438
|
|
|
3356
|
-
## How You Communicate
|
|
3357
|
-
|
|
3358
3439
|
You are running inside **Agent Hub**, a messaging platform for agent teams.
|
|
3359
3440
|
|
|
3360
|
-
- Messages from other team members arrive as your prompt input
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
- **
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
a reply — if you have nothing new for the recipient, output nothing and
|
|
3368
|
-
the runtime will end the turn silently.
|
|
3369
|
-
Your role and responsibilities are injected via the Hub-managed system prompt.
|
|
3370
|
-
|
|
3371
|
-
## Environment Variables
|
|
3372
|
-
|
|
3373
|
-
These are injected automatically when the agent process starts:
|
|
3374
|
-
|
|
3375
|
-
| Variable | Description |
|
|
3376
|
-
|----------|-------------|
|
|
3377
|
-
| \`FIRST_TREE_HUB_SERVER_URL\` | Server address for API calls |
|
|
3378
|
-
| \`FIRST_TREE_HUB_ACCESS_TOKEN\` | User member access JWT (short-lived) |
|
|
3379
|
-
| \`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. |
|
|
3380
|
-
| \`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. |
|
|
3381
|
-
|
|
3382
|
-
The \`first-tree-hub\` CLI reads these automatically — no extra setup needed.
|
|
3441
|
+
- Messages from other team members arrive as your prompt input. Each message has a
|
|
3442
|
+
\`[From: <agent-name>]\` header — that name is what you pass back to \`chat send\`.
|
|
3443
|
+
- **Your final text response is automatically delivered** to the chat — just respond normally.
|
|
3444
|
+
- **Stay silent when you have nothing to add.** Not every message needs a reply.
|
|
3445
|
+
If you have nothing new for the recipient, output nothing and the runtime ends the turn.
|
|
3446
|
+
- For **proactive communication** (other agents, other chats, or different format),
|
|
3447
|
+
use the \`first-tree-hub\` CLI below.
|
|
3383
3448
|
|
|
3384
3449
|
## Sending Messages
|
|
3385
3450
|
|
|
3386
|
-
|
|
3387
|
-
attaches the \`Authorization\` + \`X-Agent-Id\` headers automatically:
|
|
3451
|
+
The CLI auto-reads its config from env — no setup needed.
|
|
3388
3452
|
|
|
3389
3453
|
\`\`\`bash
|
|
3390
|
-
# Send to
|
|
3391
|
-
# (NOT a uuid; uuids in chat history / participant lists are not accepted).
|
|
3392
|
-
# Run \`first-tree-hub agent list\` to see available names.
|
|
3393
|
-
#
|
|
3394
|
-
# Routing: if the recipient is a participant of your current chat (typically
|
|
3395
|
-
# the case in a group chat where someone @-mentioned you to talk to them),
|
|
3396
|
-
# the message stays in that chat. Otherwise it falls back to a direct chat
|
|
3397
|
-
# between you and the recipient. You don't need to think about which.
|
|
3454
|
+
# Send to an agent by NAME (uuids are NOT accepted — run \`first-tree-hub agent list\` for names)
|
|
3398
3455
|
first-tree-hub chat send <agentName> "your message"
|
|
3399
3456
|
|
|
3400
|
-
#
|
|
3401
|
-
# to address a chat your current session is NOT bound to.
|
|
3457
|
+
# Address a specific chat (only when not your current chat)
|
|
3402
3458
|
first-tree-hub chat send --chat <chatId> "your message"
|
|
3403
3459
|
|
|
3404
|
-
#
|
|
3405
|
-
first-tree-hub chat send <agentName> -f markdown "**bold**
|
|
3460
|
+
# Markdown format (default is text)
|
|
3461
|
+
first-tree-hub chat send <agentName> -f markdown "**bold**"
|
|
3406
3462
|
|
|
3407
|
-
# Reply to a
|
|
3408
|
-
first-tree-hub chat send <agentName> --reply-to <messageId> "reply
|
|
3463
|
+
# Reply to a message
|
|
3464
|
+
first-tree-hub chat send <agentName> --reply-to <messageId> "reply"
|
|
3409
3465
|
|
|
3410
|
-
# Pipe long content via stdin
|
|
3411
|
-
echo "long
|
|
3466
|
+
# Pipe long / multiline content via stdin
|
|
3467
|
+
echo "long body" | first-tree-hub chat send <agentName>
|
|
3412
3468
|
\`\`\`
|
|
3413
3469
|
|
|
3414
|
-
|
|
3415
|
-
> but they are NOT accepted by \`chat send\` — always use the name.
|
|
3470
|
+
**Content rules (important):**
|
|
3416
3471
|
|
|
3417
|
-
|
|
3472
|
+
- Pass content as a **raw string** — never \`JSON.stringify\` it first. Wrapping in
|
|
3473
|
+
outer quotes + \`\\n\` escapes produces a literal \`"@x ...\\n..."\` that the UI
|
|
3474
|
+
cannot render as markdown.
|
|
3475
|
+
- For multi-line / markdown / special chars (quotes, \`$\`, backticks, newlines),
|
|
3476
|
+
use **stdin** with real newlines, plus \`-f markdown\`.
|
|
3418
3477
|
`;
|
|
3419
3478
|
}
|
|
3420
3479
|
function resolveGitRepoTargetPath(workspace, localPath) {
|
|
@@ -4691,6 +4750,7 @@ const createClaudeCodeHandler = (config) => {
|
|
|
4691
4750
|
/** Worktrees materialised for this session — each entry removed on shutdown. */
|
|
4692
4751
|
const ownedWorktrees = [];
|
|
4693
4752
|
async function toSDKUserMessage(message, sessionCtx, sessionId) {
|
|
4753
|
+
emitContextTreeUsage(sessionCtx);
|
|
4694
4754
|
if (message.format === "file") {
|
|
4695
4755
|
const senderLabel = message.senderId ? await sessionCtx.resolveSenderLabel(message.senderId) : "";
|
|
4696
4756
|
const prefix = senderLabel ? `[From: ${senderLabel}]\n\n` : "";
|
|
@@ -4753,6 +4813,16 @@ const createClaudeCodeHandler = (config) => {
|
|
|
4753
4813
|
session_id: sessionId
|
|
4754
4814
|
};
|
|
4755
4815
|
}
|
|
4816
|
+
function emitContextTreeUsage(sessionCtx) {
|
|
4817
|
+
if (!contextTreePath) return;
|
|
4818
|
+
sessionCtx.emitEvent({
|
|
4819
|
+
kind: "context_tree_usage",
|
|
4820
|
+
payload: {
|
|
4821
|
+
purpose: "design_decision",
|
|
4822
|
+
treeRepoUrl: contextTreeRepoUrl
|
|
4823
|
+
}
|
|
4824
|
+
});
|
|
4825
|
+
}
|
|
4756
4826
|
/**
|
|
4757
4827
|
* Build env for the child Claude Code process.
|
|
4758
4828
|
*
|
|
@@ -5356,6 +5426,16 @@ const createCodexHandler = (config) => {
|
|
|
5356
5426
|
function toCodexInput(message, sessionCtx) {
|
|
5357
5427
|
return sessionCtx.formatInboundContent(message).then((text) => text);
|
|
5358
5428
|
}
|
|
5429
|
+
function emitContextTreeUsage(sessionCtx) {
|
|
5430
|
+
if (!contextTreePath) return;
|
|
5431
|
+
sessionCtx.emitEvent({
|
|
5432
|
+
kind: "context_tree_usage",
|
|
5433
|
+
payload: {
|
|
5434
|
+
purpose: "design_decision",
|
|
5435
|
+
treeRepoUrl: contextTreeRepoUrl
|
|
5436
|
+
}
|
|
5437
|
+
});
|
|
5438
|
+
}
|
|
5359
5439
|
async function prepareGitWorktrees(payload, workspaceCwd, sessionCtx) {
|
|
5360
5440
|
if (!gitMirrorManager) return;
|
|
5361
5441
|
const branchAgentKey = agentName ?? sessionCtx.agent.agentId;
|
|
@@ -5484,6 +5564,7 @@ const createCodexHandler = (config) => {
|
|
|
5484
5564
|
if (!activeThread) return;
|
|
5485
5565
|
const abort = new AbortController();
|
|
5486
5566
|
currentAbort = abort;
|
|
5567
|
+
emitContextTreeUsage(sessionCtx);
|
|
5487
5568
|
sessionCtx.setRuntimeState("working");
|
|
5488
5569
|
const assistantTexts = [];
|
|
5489
5570
|
let turnFailed = false;
|
|
@@ -5968,13 +6049,17 @@ var Deduplicator = class {
|
|
|
5968
6049
|
};
|
|
5969
6050
|
function createResultSink(deps) {
|
|
5970
6051
|
async function buildMetadata(trigger) {
|
|
5971
|
-
|
|
5972
|
-
const
|
|
5973
|
-
if (
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
6052
|
+
const metadata = {};
|
|
6053
|
+
const documentBasePath = await deps.getDocumentBasePath?.();
|
|
6054
|
+
if (documentBasePath) metadata.documentContext = documentContextSchema.parse({ basePath: documentBasePath });
|
|
6055
|
+
if (trigger && trigger.senderId !== deps.agent.agentId) {
|
|
6056
|
+
const participants = await deps.participants.get();
|
|
6057
|
+
if (participants.length <= 2) {
|
|
6058
|
+
const peer = participants.find((p) => p.agentId === trigger.senderId);
|
|
6059
|
+
if (!peer || peer.mode === "mention_only") metadata.mentions = [trigger.senderId];
|
|
6060
|
+
} else metadata.mentions = [trigger.senderId];
|
|
6061
|
+
}
|
|
6062
|
+
return Object.keys(metadata).length > 0 ? metadata : void 0;
|
|
5978
6063
|
}
|
|
5979
6064
|
return async function forwardResult(text) {
|
|
5980
6065
|
if (text.trim().length === 0) {
|
|
@@ -6065,6 +6150,16 @@ var SessionRegistry = class {
|
|
|
6065
6150
|
}
|
|
6066
6151
|
}
|
|
6067
6152
|
};
|
|
6153
|
+
function documentBasePathFromRuntimeConfig(payload) {
|
|
6154
|
+
if (payload.gitRepos.length !== 1) return null;
|
|
6155
|
+
const repo = payload.gitRepos[0];
|
|
6156
|
+
if (!repo) return null;
|
|
6157
|
+
const localPath = repoLocalPath(repo).trim();
|
|
6158
|
+
return localPath.length > 0 ? localPath : null;
|
|
6159
|
+
}
|
|
6160
|
+
function repoLocalPath(repo) {
|
|
6161
|
+
return repo.localPath ?? deriveRepoLocalPath(repo.url);
|
|
6162
|
+
}
|
|
6068
6163
|
/**
|
|
6069
6164
|
* Manages per-chat session entries with session-oriented handler lifecycle.
|
|
6070
6165
|
*
|
|
@@ -6554,7 +6649,8 @@ var SessionManager = class {
|
|
|
6554
6649
|
this.currentTrigger.delete(chatId);
|
|
6555
6650
|
},
|
|
6556
6651
|
log,
|
|
6557
|
-
participants
|
|
6652
|
+
participants,
|
|
6653
|
+
getDocumentBasePath: () => this.resolveDocumentBasePath(log)
|
|
6558
6654
|
});
|
|
6559
6655
|
const envCtx = {
|
|
6560
6656
|
sdk: this.config.sdk,
|
|
@@ -6582,6 +6678,16 @@ var SessionManager = class {
|
|
|
6582
6678
|
resolveSenderLabel: async (senderId) => resolveSenderLabel(senderId, await participants.get())
|
|
6583
6679
|
};
|
|
6584
6680
|
}
|
|
6681
|
+
async resolveDocumentBasePath(log) {
|
|
6682
|
+
if (!this.config.agentConfigCache) return null;
|
|
6683
|
+
try {
|
|
6684
|
+
const { payload } = await this.config.agentConfigCache.refreshIfNewer(this.config.agentIdentity.agentId, 0);
|
|
6685
|
+
return documentBasePathFromRuntimeConfig(payload);
|
|
6686
|
+
} catch (err) {
|
|
6687
|
+
log(`document preview base path unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
6688
|
+
return null;
|
|
6689
|
+
}
|
|
6690
|
+
}
|
|
6585
6691
|
/** Update per-session runtime state and recompute aggregate. Only active sessions may update. */
|
|
6586
6692
|
setSessionRuntimeState(chatId, state) {
|
|
6587
6693
|
const session = this.sessions.get(chatId);
|
|
@@ -9553,7 +9659,7 @@ async function onboardCreate(args) {
|
|
|
9553
9659
|
}
|
|
9554
9660
|
const runtimeAgent = args.type === "human" ? args.assistant : args.id;
|
|
9555
9661
|
if (args.feishuBotAppId && args.feishuBotAppSecret) {
|
|
9556
|
-
const { bindFeishuBot } = await import("./feishu-
|
|
9662
|
+
const { bindFeishuBot } = await import("./feishu-BGx71p5s.mjs").then((n) => n.r);
|
|
9557
9663
|
const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
|
|
9558
9664
|
if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
|
|
9559
9665
|
else {
|
|
@@ -10766,7 +10872,7 @@ function createFeedbackHandler(config) {
|
|
|
10766
10872
|
return { handle };
|
|
10767
10873
|
}
|
|
10768
10874
|
//#endregion
|
|
10769
|
-
//#region ../server/dist/app-
|
|
10875
|
+
//#region ../server/dist/app-BcZq1C1l.mjs
|
|
10770
10876
|
var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
|
|
10771
10877
|
init_esm();
|
|
10772
10878
|
var __defProp = Object.defineProperty;
|
|
@@ -11376,6 +11482,21 @@ async function ensureDefaultOrganization(db) {
|
|
|
11376
11482
|
*/
|
|
11377
11483
|
const RESERVED_AGENT_NAME_PREFIX = "__";
|
|
11378
11484
|
/**
|
|
11485
|
+
* Derive the relative URL clients should use to fetch a manager-uploaded
|
|
11486
|
+
* avatar image. Returns `null` when no image is set. Embeds the upload
|
|
11487
|
+
* timestamp as `?v=<epoch>` so a fresh upload busts any browser cache
|
|
11488
|
+
* that may have memoised the previous version.
|
|
11489
|
+
*
|
|
11490
|
+
* Auth: the image route is intentionally public read — the URL leaks no
|
|
11491
|
+
* more than the agent's UUID, which is already required to address it.
|
|
11492
|
+
* Keeping it unauthenticated lets `<img src>` render without bespoke
|
|
11493
|
+
* fetch-and-blob plumbing.
|
|
11494
|
+
*/
|
|
11495
|
+
function agentAvatarImageUrl(uuid, updatedAt) {
|
|
11496
|
+
if (!updatedAt) return null;
|
|
11497
|
+
return `/api/v1/agents/${uuid}/avatar?v=${updatedAt.getTime()}`;
|
|
11498
|
+
}
|
|
11499
|
+
/**
|
|
11379
11500
|
* True iff `clients.metadata.capabilities` is a non-empty object — i.e. the
|
|
11380
11501
|
* client has reported at least one runtime probe result. Used to distinguish
|
|
11381
11502
|
* "we don't know what's installed yet" (empty / never reported) from
|
|
@@ -11634,6 +11755,8 @@ async function listAgentsForAdmin(db, scope, limit, cursor) {
|
|
|
11634
11755
|
managerId: agents.managerId,
|
|
11635
11756
|
clientId: agents.clientId,
|
|
11636
11757
|
runtimeProvider: agents.runtimeProvider,
|
|
11758
|
+
avatarColorToken: agents.avatarColorToken,
|
|
11759
|
+
avatarImageUpdatedAt: agents.avatarImageUpdatedAt,
|
|
11637
11760
|
createdAt: agents.createdAt,
|
|
11638
11761
|
updatedAt: agents.updatedAt,
|
|
11639
11762
|
presenceStatus: agentPresence.status,
|
|
@@ -11672,6 +11795,8 @@ async function listAgentsForMember(db, scope, limit, cursor, type) {
|
|
|
11672
11795
|
managerId: agents.managerId,
|
|
11673
11796
|
clientId: agents.clientId,
|
|
11674
11797
|
runtimeProvider: agents.runtimeProvider,
|
|
11798
|
+
avatarColorToken: agents.avatarColorToken,
|
|
11799
|
+
avatarImageUpdatedAt: agents.avatarImageUpdatedAt,
|
|
11675
11800
|
createdAt: agents.createdAt,
|
|
11676
11801
|
updatedAt: agents.updatedAt,
|
|
11677
11802
|
presenceStatus: agentPresence.status,
|
|
@@ -11708,6 +11833,7 @@ async function updateAgent(db, uuid, data) {
|
|
|
11708
11833
|
}
|
|
11709
11834
|
if (data.visibility !== void 0) updates.visibility = data.visibility;
|
|
11710
11835
|
if (data.metadata !== void 0) updates.metadata = data.metadata;
|
|
11836
|
+
if (data.avatarColorToken !== void 0) updates.avatarColorToken = data.avatarColorToken;
|
|
11711
11837
|
if (data.managerId !== void 0) {
|
|
11712
11838
|
if (data.managerId === null) throw new BadRequestError("managerId cannot be cleared — every agent must have a manager");
|
|
11713
11839
|
const [manager] = await db.select({
|
|
@@ -11814,6 +11940,63 @@ async function deleteAgent(db, uuid) {
|
|
|
11814
11940
|
if (!agent) throw new Error("Unexpected: UPDATE RETURNING produced no row");
|
|
11815
11941
|
return agent;
|
|
11816
11942
|
}
|
|
11943
|
+
/**
|
|
11944
|
+
* Supported avatar-image MIME types. The web client always uploads WEBP after
|
|
11945
|
+
* its own resize step; we accept PNG/JPEG too so a caller using the raw HTTP
|
|
11946
|
+
* API (curl, scripts) doesn't have to re-encode. Anything else is rejected at
|
|
11947
|
+
* the boundary — we never store an unknown content type.
|
|
11948
|
+
*/
|
|
11949
|
+
const SUPPORTED_AVATAR_IMAGE_MIMES = [
|
|
11950
|
+
"image/webp",
|
|
11951
|
+
"image/png",
|
|
11952
|
+
"image/jpeg"
|
|
11953
|
+
];
|
|
11954
|
+
/** Hard server-side ceiling for the stored bytea blob. Client pre-resizes to ~50KB. */
|
|
11955
|
+
const MAX_AVATAR_IMAGE_BYTES = 512 * 1024;
|
|
11956
|
+
function isSupportedAvatarMime(mime) {
|
|
11957
|
+
return SUPPORTED_AVATAR_IMAGE_MIMES.find((m) => m === mime) !== void 0;
|
|
11958
|
+
}
|
|
11959
|
+
/**
|
|
11960
|
+
* Fetch the avatar image blob for an agent. Returns `null` when no image
|
|
11961
|
+
* is set (the column is NULL). The data + mime pair is always coherent
|
|
11962
|
+
* (set/cleared together by the service writes below).
|
|
11963
|
+
*/
|
|
11964
|
+
async function getAgentAvatarImage(db, uuid) {
|
|
11965
|
+
const [row] = await db.select({
|
|
11966
|
+
data: agents.avatarImageData,
|
|
11967
|
+
mime: agents.avatarImageMime,
|
|
11968
|
+
updatedAt: agents.avatarImageUpdatedAt
|
|
11969
|
+
}).from(agents).where(and(eq(agents.uuid, uuid), ne(agents.status, AGENT_STATUSES.DELETED))).limit(1);
|
|
11970
|
+
if (!row || !row.data || !row.mime || !row.updatedAt) return null;
|
|
11971
|
+
return {
|
|
11972
|
+
data: row.data,
|
|
11973
|
+
mime: row.mime,
|
|
11974
|
+
updatedAt: row.updatedAt
|
|
11975
|
+
};
|
|
11976
|
+
}
|
|
11977
|
+
/** Replace (or set) an agent's avatar image. Validates mime + size. */
|
|
11978
|
+
async function setAgentAvatarImage(db, uuid, data, mime) {
|
|
11979
|
+
if (!isSupportedAvatarMime(mime)) throw new BadRequestError(`Unsupported avatar image type "${mime}". Use PNG, JPEG, or WEBP.`);
|
|
11980
|
+
if (data.length === 0) throw new BadRequestError("Avatar image payload is empty.");
|
|
11981
|
+
if (data.length > 524288) throw new BadRequestError(`Avatar image is too large (${data.length} bytes; max ${MAX_AVATAR_IMAGE_BYTES}).`);
|
|
11982
|
+
const now = /* @__PURE__ */ new Date();
|
|
11983
|
+
if ((await db.update(agents).set({
|
|
11984
|
+
avatarImageData: data,
|
|
11985
|
+
avatarImageMime: mime,
|
|
11986
|
+
avatarImageUpdatedAt: now,
|
|
11987
|
+
updatedAt: now
|
|
11988
|
+
}).where(and(eq(agents.uuid, uuid), ne(agents.status, AGENT_STATUSES.DELETED))).returning({ uuid: agents.uuid })).length === 0) throw new NotFoundError(`Agent "${uuid}" not found`);
|
|
11989
|
+
return now;
|
|
11990
|
+
}
|
|
11991
|
+
/** Clear an agent's avatar image (falls back to color + initial). */
|
|
11992
|
+
async function clearAgentAvatarImage(db, uuid) {
|
|
11993
|
+
if ((await db.update(agents).set({
|
|
11994
|
+
avatarImageData: null,
|
|
11995
|
+
avatarImageMime: null,
|
|
11996
|
+
avatarImageUpdatedAt: null,
|
|
11997
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
11998
|
+
}).where(and(eq(agents.uuid, uuid), ne(agents.status, AGENT_STATUSES.DELETED))).returning({ uuid: agents.uuid })).length === 0) throw new NotFoundError(`Agent "${uuid}" not found`);
|
|
11999
|
+
}
|
|
11817
12000
|
const log$5 = createLogger$1("AgentFeishuBot");
|
|
11818
12001
|
async function agentFeishuBotRoutes(app) {
|
|
11819
12002
|
/**
|
|
@@ -13122,6 +13305,18 @@ async function listEvents(db, agentId, chatId, options) {
|
|
|
13122
13305
|
async function clearEvents(db, agentId, chatId) {
|
|
13123
13306
|
await db.delete(sessionEvents).where(and(eq(sessionEvents.agentId, agentId), eq(sessionEvents.chatId, chatId)));
|
|
13124
13307
|
}
|
|
13308
|
+
async function summarizeContextTreeUsage(db, organizationId, windowDays) {
|
|
13309
|
+
const since = /* @__PURE__ */ new Date(Date.now() - windowDays * 24 * 60 * 60 * 1e3);
|
|
13310
|
+
const [row] = await db.select({
|
|
13311
|
+
agentCount: sql`count(distinct ${sessionEvents.agentId})::int`,
|
|
13312
|
+
usageCount: sql`count(*)::int`
|
|
13313
|
+
}).from(sessionEvents).innerJoin(agents, eq(agents.uuid, sessionEvents.agentId)).where(and(eq(agents.organizationId, organizationId), eq(sessionEvents.kind, "context_tree_usage"), gte(sessionEvents.createdAt, since)));
|
|
13314
|
+
return {
|
|
13315
|
+
windowDays,
|
|
13316
|
+
agentCount: row?.agentCount ?? 0,
|
|
13317
|
+
usageCount: row?.usageCount ?? 0
|
|
13318
|
+
};
|
|
13319
|
+
}
|
|
13125
13320
|
/**
|
|
13126
13321
|
* Default per-agent in-flight cap when `server.inbox.maxInFlightPerAgent` is
|
|
13127
13322
|
* unset. Mirrors the schema default so a hub running without an explicit
|
|
@@ -13772,6 +13967,22 @@ async function agentActivityRoutes(app) {
|
|
|
13772
13967
|
});
|
|
13773
13968
|
}
|
|
13774
13969
|
/**
|
|
13970
|
+
* Project a DB agent row into its wire shape. Strips the inline image
|
|
13971
|
+
* `avatarImageData` (large bytea, only meant for the image-serve route)
|
|
13972
|
+
* and synthesises the public `avatarImageUrl` from the upload timestamp.
|
|
13973
|
+
* `createdAt`/`updatedAt` are coerced to ISO strings so the response is
|
|
13974
|
+
* pure JSON.
|
|
13975
|
+
*/
|
|
13976
|
+
function serializeAgent(agent) {
|
|
13977
|
+
const { avatarImageData: _data, avatarImageMime: _mime, avatarImageUpdatedAt, createdAt, updatedAt, ...rest } = agent;
|
|
13978
|
+
return {
|
|
13979
|
+
...rest,
|
|
13980
|
+
createdAt: createdAt.toISOString(),
|
|
13981
|
+
updatedAt: updatedAt.toISOString(),
|
|
13982
|
+
avatarImageUrl: agentAvatarImageUrl(agent.uuid, avatarImageUpdatedAt ?? null)
|
|
13983
|
+
};
|
|
13984
|
+
}
|
|
13985
|
+
/**
|
|
13775
13986
|
* Class C — resource-scoped per-agent routes. Mounted at
|
|
13776
13987
|
* `/api/v1/agents/:uuid/...`. The agent's UUID locates its org
|
|
13777
13988
|
* intrinsically; `requireAgentAccess` resolves the caller's membership in
|
|
@@ -13800,11 +14011,7 @@ async function agentRoutes(app) {
|
|
|
13800
14011
|
}
|
|
13801
14012
|
app.get("/:uuid", async (request) => {
|
|
13802
14013
|
const { agent } = await requireAgentAccess(request, app.db, "visible");
|
|
13803
|
-
return
|
|
13804
|
-
...agent,
|
|
13805
|
-
createdAt: agent.createdAt.toISOString(),
|
|
13806
|
-
updatedAt: agent.updatedAt.toISOString()
|
|
13807
|
-
};
|
|
14014
|
+
return serializeAgent(agent);
|
|
13808
14015
|
});
|
|
13809
14016
|
app.patch("/:uuid", { config: { otelRecordBody: true } }, async (request) => {
|
|
13810
14017
|
const { scope } = await requireAgentAccess(request, app.db, "manage");
|
|
@@ -13813,22 +14020,14 @@ async function agentRoutes(app) {
|
|
|
13813
14020
|
const before = body.clientId !== void 0 ? await getAgent(app.db, request.params.uuid) : null;
|
|
13814
14021
|
const agent = await updateAgent(app.db, request.params.uuid, body);
|
|
13815
14022
|
if (before && before.clientId === null && agent.clientId !== null) notifyClientAgentPinned(agent);
|
|
13816
|
-
return
|
|
13817
|
-
...agent,
|
|
13818
|
-
createdAt: agent.createdAt.toISOString(),
|
|
13819
|
-
updatedAt: agent.updatedAt.toISOString()
|
|
13820
|
-
};
|
|
14023
|
+
return serializeAgent(agent);
|
|
13821
14024
|
});
|
|
13822
14025
|
app.patch("/:uuid/rebind", { config: { otelRecordBody: true } }, async (request) => {
|
|
13823
14026
|
await requireAgentAccess(request, app.db, "manage");
|
|
13824
14027
|
const body = rebindAgentSchema.parse(request.body);
|
|
13825
14028
|
const agent = await rebindAgent(app.db, request.params.uuid, body);
|
|
13826
14029
|
notifyClientAgentPinned(agent);
|
|
13827
|
-
return
|
|
13828
|
-
...agent,
|
|
13829
|
-
createdAt: agent.createdAt.toISOString(),
|
|
13830
|
-
updatedAt: agent.updatedAt.toISOString()
|
|
13831
|
-
};
|
|
14030
|
+
return serializeAgent(agent);
|
|
13832
14031
|
});
|
|
13833
14032
|
app.post("/:uuid/disconnect", async (request, reply) => {
|
|
13834
14033
|
await requireAgentAccess(request, app.db, "manage");
|
|
@@ -13838,27 +14037,35 @@ async function agentRoutes(app) {
|
|
|
13838
14037
|
});
|
|
13839
14038
|
app.post("/:uuid/suspend", async (request) => {
|
|
13840
14039
|
await requireAgentAccess(request, app.db, "manage");
|
|
13841
|
-
|
|
13842
|
-
return {
|
|
13843
|
-
...agent,
|
|
13844
|
-
createdAt: agent.createdAt.toISOString(),
|
|
13845
|
-
updatedAt: agent.updatedAt.toISOString()
|
|
13846
|
-
};
|
|
14040
|
+
return serializeAgent(await suspendAgent(app.db, request.params.uuid));
|
|
13847
14041
|
});
|
|
13848
14042
|
app.post("/:uuid/reactivate", async (request) => {
|
|
13849
14043
|
await requireAgentAccess(request, app.db, "manage");
|
|
13850
|
-
|
|
13851
|
-
return {
|
|
13852
|
-
...agent,
|
|
13853
|
-
createdAt: agent.createdAt.toISOString(),
|
|
13854
|
-
updatedAt: agent.updatedAt.toISOString()
|
|
13855
|
-
};
|
|
14044
|
+
return serializeAgent(await reactivateAgent(app.db, request.params.uuid));
|
|
13856
14045
|
});
|
|
13857
14046
|
app.delete("/:uuid", async (request, reply) => {
|
|
13858
14047
|
await requireAgentAccess(request, app.db, "manage");
|
|
13859
14048
|
await deleteAgent(app.db, request.params.uuid);
|
|
13860
14049
|
return reply.status(204).send();
|
|
13861
14050
|
});
|
|
14051
|
+
app.addContentTypeParser(/^image\//, { parseAs: "buffer" }, (_req, body, done) => {
|
|
14052
|
+
done(null, body);
|
|
14053
|
+
});
|
|
14054
|
+
app.put("/:uuid/avatar", { bodyLimit: MAX_AVATAR_IMAGE_BYTES + 1024 }, async (request, reply) => {
|
|
14055
|
+
await requireAgentAccess(request, app.db, "manage");
|
|
14056
|
+
const contentType = request.headers["content-type"];
|
|
14057
|
+
if (typeof contentType !== "string" || !contentType.startsWith("image/")) throw new BadRequestError(`Avatar upload requires an image/* Content-Type. Supported: ${SUPPORTED_AVATAR_IMAGE_MIMES.join(", ")}.`);
|
|
14058
|
+
const mime = contentType.split(";")[0]?.trim() ?? "";
|
|
14059
|
+
const body = request.body;
|
|
14060
|
+
if (!Buffer.isBuffer(body)) throw new BadRequestError("Avatar upload body must be raw image bytes.");
|
|
14061
|
+
const updatedAt = await setAgentAvatarImage(app.db, request.params.uuid, body, mime);
|
|
14062
|
+
return reply.status(200).send({ avatarImageUrl: agentAvatarImageUrl(request.params.uuid, updatedAt) });
|
|
14063
|
+
});
|
|
14064
|
+
app.delete("/:uuid/avatar", async (request, reply) => {
|
|
14065
|
+
await requireAgentAccess(request, app.db, "manage");
|
|
14066
|
+
await clearAgentAvatarImage(app.db, request.params.uuid);
|
|
14067
|
+
return reply.status(204).send();
|
|
14068
|
+
});
|
|
13862
14069
|
app.post("/:uuid/test", async (request, reply) => {
|
|
13863
14070
|
const { uuid } = request.params;
|
|
13864
14071
|
const { agent: targetAgent } = await requireAgentAccess(request, app.db, "manage");
|
|
@@ -13967,6 +14174,23 @@ async function agentRoutes(app) {
|
|
|
13967
14174
|
});
|
|
13968
14175
|
}
|
|
13969
14176
|
/**
|
|
14177
|
+
* Public read-only route for agent avatar images. Mounted outside the
|
|
14178
|
+
* member-JWT auth scope so `<img src>` works without bespoke fetch-and-blob
|
|
14179
|
+
* plumbing. Reading an avatar leaks no more than the agent's UUID — which
|
|
14180
|
+
* is already required to address the route — and the UUID itself is only
|
|
14181
|
+
* exposed through authenticated agent-list calls.
|
|
14182
|
+
*/
|
|
14183
|
+
async function publicAgentAvatarRoutes(app) {
|
|
14184
|
+
app.get("/:uuid/avatar", async (request, reply) => {
|
|
14185
|
+
const image = await getAgentAvatarImage(app.db, request.params.uuid);
|
|
14186
|
+
if (!image) return reply.status(404).send({ error: "Avatar not set" });
|
|
14187
|
+
reply.header("Content-Type", image.mime);
|
|
14188
|
+
reply.header("Cache-Control", "public, max-age=2592000, immutable");
|
|
14189
|
+
reply.header("ETag", `"${image.updatedAt.getTime()}"`);
|
|
14190
|
+
return reply.send(image.data);
|
|
14191
|
+
});
|
|
14192
|
+
}
|
|
14193
|
+
/**
|
|
13970
14194
|
* Class C — `/api/v1/agents/:uuid/config`. Runtime config (system prompt,
|
|
13971
14195
|
* tools, env) is behavior-sensitive — gated on `manage`, not `visible`.
|
|
13972
14196
|
*/
|
|
@@ -15012,6 +15236,22 @@ async function listActiveMemberships(db, userId) {
|
|
|
15012
15236
|
}).from(members).innerJoin(organizations, eq(members.organizationId, organizations.id)).where(and(eq(members.userId, userId), eq(members.status, "active"))).orderBy(desc(members.createdAt));
|
|
15013
15237
|
}
|
|
15014
15238
|
/**
|
|
15239
|
+
* Count ACTIVE members per org, restricted to the given org IDs. Returns a
|
|
15240
|
+
* Map keyed by `organizationId`; orgs absent from the result simply have
|
|
15241
|
+
* zero active members (shouldn't happen in practice — the caller always
|
|
15242
|
+
* passes orgs the user is a member of — but the Map shape lets callers do
|
|
15243
|
+
* `counts.get(orgId) ?? 0` defensively). Used by `/me` to surface
|
|
15244
|
+
* `orgHasOtherMembers` per membership without N+1 queries.
|
|
15245
|
+
*/
|
|
15246
|
+
async function countActiveMembersByOrgs(db, organizationIds) {
|
|
15247
|
+
if (organizationIds.length === 0) return /* @__PURE__ */ new Map();
|
|
15248
|
+
const rows = await db.select({
|
|
15249
|
+
organizationId: members.organizationId,
|
|
15250
|
+
count: sql`count(*)::int`
|
|
15251
|
+
}).from(members).where(and(inArray(members.organizationId, organizationIds), eq(members.status, "active"))).groupBy(members.organizationId);
|
|
15252
|
+
return new Map(rows.map((r) => [r.organizationId, r.count]));
|
|
15253
|
+
}
|
|
15254
|
+
/**
|
|
15015
15255
|
* Pick the most recently joined active membership — used after OAuth login
|
|
15016
15256
|
* when the user already has at least one team but no `next` was specified.
|
|
15017
15257
|
*/
|
|
@@ -15803,16 +16043,16 @@ function decodeCursor(cursor) {
|
|
|
15803
16043
|
return null;
|
|
15804
16044
|
}
|
|
15805
16045
|
}
|
|
15806
|
-
const { ACTIVE, ARCHIVED, DELETED } = CHAT_ENGAGEMENT_STATUSES;
|
|
16046
|
+
const { ACTIVE: ACTIVE$1, ARCHIVED: ARCHIVED$1, DELETED } = CHAT_ENGAGEMENT_STATUSES;
|
|
15807
16047
|
/**
|
|
15808
16048
|
* SQL predicate for each engagement view tab. `deleted` is never a valid view
|
|
15809
16049
|
* value — deleted rows are reachable only through `GET /chats/:chatId` + the
|
|
15810
16050
|
* Restore banner on the chat detail page.
|
|
15811
16051
|
*/
|
|
15812
16052
|
const ENGAGEMENT_VIEW_PREDICATE = {
|
|
15813
|
-
active: sql`COALESCE(cus.engagement_status, ${ACTIVE}) = ${ACTIVE}`,
|
|
15814
|
-
archived: sql`COALESCE(cus.engagement_status, ${ACTIVE}) = ${ARCHIVED}`,
|
|
15815
|
-
all: sql`COALESCE(cus.engagement_status, ${ACTIVE}) IN (${ACTIVE}, ${ARCHIVED})`
|
|
16053
|
+
active: sql`COALESCE(cus.engagement_status, ${ACTIVE$1}) = ${ACTIVE$1}`,
|
|
16054
|
+
archived: sql`COALESCE(cus.engagement_status, ${ACTIVE$1}) = ${ARCHIVED$1}`,
|
|
16055
|
+
all: sql`COALESCE(cus.engagement_status, ${ACTIVE$1}) IN (${ACTIVE$1}, ${ARCHIVED$1})`
|
|
15816
16056
|
};
|
|
15817
16057
|
/**
|
|
15818
16058
|
* Write the caller's engagement state for this chat. UPSERT into
|
|
@@ -15840,7 +16080,26 @@ async function setChatEngagement(db, chatId, agentId, status) {
|
|
|
15840
16080
|
*/
|
|
15841
16081
|
async function getCallerEngagement(db, chatId, agentId) {
|
|
15842
16082
|
const [row] = await db.select({ engagementStatus: chatUserState.engagementStatus }).from(chatUserState).where(and(eq(chatUserState.chatId, chatId), eq(chatUserState.agentId, agentId))).limit(1);
|
|
15843
|
-
return row?.engagementStatus ?? ACTIVE;
|
|
16083
|
+
return row?.engagementStatus ?? ACTIVE$1;
|
|
16084
|
+
}
|
|
16085
|
+
const KNOWN_NON_MANUAL_PREDICATE = sql`(
|
|
16086
|
+
(c.metadata->>'source' = 'github' AND c.metadata->>'entityType' IN (${sql.join(GITHUB_ENTITY_TYPES.map((t) => sql`${t}`), sql.raw(", "))}))
|
|
16087
|
+
OR c.metadata->>'source' = 'feishu'
|
|
16088
|
+
)`;
|
|
16089
|
+
const chatSourceSqlExpression = sql`CASE
|
|
16090
|
+
${sql.join(GITHUB_ENTITY_TYPES.map((t) => sql`WHEN c.metadata->>'source' = 'github' AND c.metadata->>'entityType' = ${t} THEN ${`github_${t}`}`), sql.raw("\n "))}
|
|
16091
|
+
WHEN c.metadata->>'source' = 'feishu' THEN 'feishu'
|
|
16092
|
+
ELSE 'manual'
|
|
16093
|
+
END`;
|
|
16094
|
+
function sourceFilterSql(source) {
|
|
16095
|
+
switch (source) {
|
|
16096
|
+
case "manual": return sql`(${KNOWN_NON_MANUAL_PREDICATE}) IS NOT TRUE`;
|
|
16097
|
+
case "github_issue": return sql`(c.metadata->>'source' = 'github' AND c.metadata->>'entityType' = 'issue')`;
|
|
16098
|
+
case "github_pull_request": return sql`(c.metadata->>'source' = 'github' AND c.metadata->>'entityType' = 'pull_request')`;
|
|
16099
|
+
case "github_discussion": return sql`(c.metadata->>'source' = 'github' AND c.metadata->>'entityType' = 'discussion')`;
|
|
16100
|
+
case "github_commit": return sql`(c.metadata->>'source' = 'github' AND c.metadata->>'entityType' = 'commit')`;
|
|
16101
|
+
case "feishu": return sql`(c.metadata->>'source' = 'feishu')`;
|
|
16102
|
+
}
|
|
15844
16103
|
}
|
|
15845
16104
|
/**
|
|
15846
16105
|
* GET /me/chats — cursor-paginated conversation list.
|
|
@@ -15866,6 +16125,7 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
|
15866
16125
|
const filterUnreadOnly = query.filter === "unread";
|
|
15867
16126
|
const filterWatchingOnly = query.filter === "watching";
|
|
15868
16127
|
const engagementPredicate = ENGAGEMENT_VIEW_PREDICATE[query.engagement];
|
|
16128
|
+
const sourcePredicate = query.source ? sourceFilterSql(query.source) : sql`TRUE`;
|
|
15869
16129
|
const cursorTsIso = cursor?.lastMessageAt ? cursor.lastMessageAt.toISOString() : null;
|
|
15870
16130
|
const cursorPredicate = !cursor ? sql`TRUE` : cursor.lastMessageAt === null ? sql`(c.last_message_at IS NULL AND c.id < ${cursor.chatId})` : sql`(c.last_message_at IS NULL
|
|
15871
16131
|
OR c.last_message_at < ${cursorTsIso}::timestamptz
|
|
@@ -15882,7 +16142,7 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
|
15882
16142
|
WHERE chat_id = c.id AND access_mode = 'speaker') AS participant_count,
|
|
15883
16143
|
cm.access_mode AS access_mode,
|
|
15884
16144
|
COALESCE(cus.unread_mention_count, 0) AS unread_mention_count,
|
|
15885
|
-
COALESCE(cus.engagement_status, ${ACTIVE}) AS engagement_status
|
|
16145
|
+
COALESCE(cus.engagement_status, ${ACTIVE$1}) AS engagement_status
|
|
15886
16146
|
FROM chats c
|
|
15887
16147
|
JOIN chat_membership cm
|
|
15888
16148
|
ON cm.chat_id = c.id AND cm.agent_id = ${humanAgentId}
|
|
@@ -15898,6 +16158,7 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
|
15898
16158
|
AND (${!filterUnreadOnly}::bool OR COALESCE(cus.unread_mention_count, 0) > 0)
|
|
15899
16159
|
AND (${!filterWatchingOnly}::bool OR cm.access_mode = 'watcher')
|
|
15900
16160
|
AND ${engagementPredicate}
|
|
16161
|
+
AND ${sourcePredicate}
|
|
15901
16162
|
AND ${cursorPredicate}
|
|
15902
16163
|
ORDER BY c.last_message_at DESC NULLS LAST, c.id DESC
|
|
15903
16164
|
LIMIT ${limit + 1}
|
|
@@ -15920,6 +16181,8 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
|
15920
16181
|
agentId: chatMembership.agentId,
|
|
15921
16182
|
displayName: agents.displayName,
|
|
15922
16183
|
type: agents.type,
|
|
16184
|
+
avatarColorToken: agents.avatarColorToken,
|
|
16185
|
+
avatarImageUpdatedAt: agents.avatarImageUpdatedAt,
|
|
15923
16186
|
sessionState: agentChatSessions.state
|
|
15924
16187
|
}).from(chatMembership).innerJoin(agents, eq(chatMembership.agentId, agents.uuid)).leftJoin(agentChatSessions, and(eq(agentChatSessions.agentId, chatMembership.agentId), eq(agentChatSessions.chatId, chatMembership.chatId))).where(and(inArray(chatMembership.chatId, chatIds), eq(chatMembership.accessMode, "speaker")));
|
|
15925
16188
|
const participantsByChat = /* @__PURE__ */ new Map();
|
|
@@ -15929,7 +16192,9 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
|
15929
16192
|
list.push({
|
|
15930
16193
|
agentId: p.agentId,
|
|
15931
16194
|
displayName: p.displayName,
|
|
15932
|
-
type: p.type
|
|
16195
|
+
type: p.type,
|
|
16196
|
+
avatarColorToken: p.avatarColorToken,
|
|
16197
|
+
avatarImageUrl: agentAvatarImageUrl(p.agentId, p.avatarImageUpdatedAt)
|
|
15933
16198
|
});
|
|
15934
16199
|
participantsByChat.set(p.chatId, list);
|
|
15935
16200
|
if (p.sessionState === "active") {
|
|
@@ -16177,6 +16442,66 @@ async function leaveMeChat(db, chatId, humanAgentId) {
|
|
|
16177
16442
|
return result;
|
|
16178
16443
|
}
|
|
16179
16444
|
/**
|
|
16445
|
+
* Used by future bell-badge / list-pill counts. The partial index
|
|
16446
|
+
* `idx_user_state_unread WHERE unread_mention_count > 0` bounds the
|
|
16447
|
+
* driving scan; we then join `chat_membership` + `chats` so the badge
|
|
16448
|
+
* stays consistent with `listMeChats`.
|
|
16449
|
+
*
|
|
16450
|
+
* Why the joins (not just a single-table count): per §11.4 a user's
|
|
16451
|
+
* `chat_user_state` row is **preserved on detach** so read state
|
|
16452
|
+
* survives a leave/rejoin cycle. Without the membership join, any
|
|
16453
|
+
* preserved row with `unread_mention_count > 0` would keep
|
|
16454
|
+
* contributing to the badge even though the chat no longer appears in
|
|
16455
|
+
* the list. The `chats` join applies the same org-scoping +
|
|
16456
|
+
* `parent_chat_id IS NULL` filter as `listMeChats` so the two counts
|
|
16457
|
+
* cannot drift in the cross-org pollution or nested-chat cases either.
|
|
16458
|
+
*
|
|
16459
|
+
* Engagement parity: deleted chats are excluded from `listMeChats`
|
|
16460
|
+
* (any `engagement` view), so the badge must exclude them too — otherwise
|
|
16461
|
+
* the user sees an unread red dot for a chat they've removed from view.
|
|
16462
|
+
*/
|
|
16463
|
+
/**
|
|
16464
|
+
* Per-source aggregate for the conversation-list tag bar.
|
|
16465
|
+
*
|
|
16466
|
+
* Returns one row per source the caller has at least one chat for, plus an
|
|
16467
|
+
* always-present `manual` entry (zero counts when there are no manual chats —
|
|
16468
|
+
* the workspace UI uses `manual` as its default tab and must render it even
|
|
16469
|
+
* when empty).
|
|
16470
|
+
*
|
|
16471
|
+
* Filtering matches `listMeChats` for the corresponding tab so the badges
|
|
16472
|
+
* cannot drift from the list: same membership join, same `parent_chat_id IS
|
|
16473
|
+
* NULL` and `organization_id` scopes, same engagement view, same
|
|
16474
|
+
* `chat_user_state.unread_mention_count` source.
|
|
16475
|
+
*/
|
|
16476
|
+
async function listMeChatSourceCounts(db, humanAgentId, organizationId, query) {
|
|
16477
|
+
const engagementPredicate = ENGAGEMENT_VIEW_PREDICATE[query.engagement];
|
|
16478
|
+
const rows = await db.execute(sql`
|
|
16479
|
+
SELECT
|
|
16480
|
+
${chatSourceSqlExpression} AS source,
|
|
16481
|
+
count(*)::int AS chat_count,
|
|
16482
|
+
count(*) FILTER (WHERE COALESCE(cus.unread_mention_count, 0) > 0)::int AS unread_chat_count
|
|
16483
|
+
FROM chats c
|
|
16484
|
+
JOIN chat_membership cm
|
|
16485
|
+
ON cm.chat_id = c.id AND cm.agent_id = ${humanAgentId}
|
|
16486
|
+
LEFT JOIN chat_user_state cus
|
|
16487
|
+
ON cus.chat_id = c.id AND cus.agent_id = ${humanAgentId}
|
|
16488
|
+
WHERE c.parent_chat_id IS NULL
|
|
16489
|
+
AND c.organization_id = ${organizationId}
|
|
16490
|
+
AND ${engagementPredicate}
|
|
16491
|
+
GROUP BY 1
|
|
16492
|
+
`);
|
|
16493
|
+
const counts = {};
|
|
16494
|
+
for (const row of rows) counts[row.source] = {
|
|
16495
|
+
chatCount: Number(row.chat_count),
|
|
16496
|
+
unreadChatCount: Number(row.unread_chat_count)
|
|
16497
|
+
};
|
|
16498
|
+
if (!counts.manual) counts.manual = {
|
|
16499
|
+
chatCount: 0,
|
|
16500
|
+
unreadChatCount: 0
|
|
16501
|
+
};
|
|
16502
|
+
return { counts };
|
|
16503
|
+
}
|
|
16504
|
+
/**
|
|
16180
16505
|
* Class C — resource-scoped chat routes. Mounted at
|
|
16181
16506
|
* `/api/v1/chats/:chatId/...`. The chat's `organizationId` locates its
|
|
16182
16507
|
* org; `requireChatAccess` resolves the caller's membership in that org
|
|
@@ -16197,7 +16522,9 @@ async function chatRoutes(app) {
|
|
|
16197
16522
|
const agentRows = participantAgentIds.length > 0 ? await app.db.select({
|
|
16198
16523
|
agentId: agents.uuid,
|
|
16199
16524
|
displayName: agents.displayName,
|
|
16200
|
-
type: agents.type
|
|
16525
|
+
type: agents.type,
|
|
16526
|
+
avatarColorToken: agents.avatarColorToken,
|
|
16527
|
+
avatarImageUpdatedAt: agents.avatarImageUpdatedAt
|
|
16201
16528
|
}).from(agents).where(inArray(agents.uuid, participantAgentIds)) : [];
|
|
16202
16529
|
const agentMeta = new Map(agentRows.map((a) => [a.agentId, a]));
|
|
16203
16530
|
const participantsForTitle = participants.map((p) => {
|
|
@@ -16205,7 +16532,9 @@ async function chatRoutes(app) {
|
|
|
16205
16532
|
return {
|
|
16206
16533
|
agentId: p.agentId,
|
|
16207
16534
|
displayName: meta?.displayName ?? p.agentId,
|
|
16208
|
-
type: meta?.type ?? "unknown"
|
|
16535
|
+
type: meta?.type ?? "unknown",
|
|
16536
|
+
avatarColorToken: meta?.avatarColorToken ?? null,
|
|
16537
|
+
avatarImageUrl: agentAvatarImageUrl(p.agentId, meta?.avatarImageUpdatedAt ?? null)
|
|
16209
16538
|
};
|
|
16210
16539
|
});
|
|
16211
16540
|
const title = resolveChatTitle(chat.topic, firstMessagePreview, participantsForTitle, scope.humanAgentId);
|
|
@@ -16684,6 +17013,9 @@ const WINDOW_DAYS = {
|
|
|
16684
17013
|
"7d": 7,
|
|
16685
17014
|
"30d": 30
|
|
16686
17015
|
};
|
|
17016
|
+
function contextTreeSnapshotWindowDays(window) {
|
|
17017
|
+
return WINDOW_DAYS[window];
|
|
17018
|
+
}
|
|
16687
17019
|
const snapshotCache = /* @__PURE__ */ new Map();
|
|
16688
17020
|
const remoteSyncPromises = /* @__PURE__ */ new Map();
|
|
16689
17021
|
const remoteLastSyncedAt = /* @__PURE__ */ new Map();
|
|
@@ -16727,6 +17059,7 @@ async function getContextTreeSnapshot(binding, window = CONTEXT_TREE_SNAPSHOT_WI
|
|
|
16727
17059
|
snapshotStatus: statusWarning?.stale ? "stale" : "active",
|
|
16728
17060
|
contextStatus: contextStatus(statusWarning),
|
|
16729
17061
|
summary,
|
|
17062
|
+
usage: emptyUsageSummary(window),
|
|
16730
17063
|
updates,
|
|
16731
17064
|
nodes: nodesWithGhosts,
|
|
16732
17065
|
edges: tree.edges,
|
|
@@ -16981,6 +17314,13 @@ function withSnapshotStatus(snapshot, syncedAt, warning) {
|
|
|
16981
17314
|
contextStatus: warning ? contextStatus(warning) : snapshot.contextStatus
|
|
16982
17315
|
};
|
|
16983
17316
|
}
|
|
17317
|
+
function emptyUsageSummary(window) {
|
|
17318
|
+
return {
|
|
17319
|
+
windowDays: WINDOW_DAYS[window],
|
|
17320
|
+
agentCount: 0,
|
|
17321
|
+
usageCount: 0
|
|
17322
|
+
};
|
|
17323
|
+
}
|
|
16984
17324
|
function isSafeBranchName(branch) {
|
|
16985
17325
|
if (branch.startsWith("-")) return false;
|
|
16986
17326
|
if (branch.includes("..") || branch.includes("@{") || branch.includes("\\")) return false;
|
|
@@ -17011,6 +17351,7 @@ function unavailableSnapshot(repo, branch, detail) {
|
|
|
17011
17351
|
removedCount: 0,
|
|
17012
17352
|
changedNodeCount: 0
|
|
17013
17353
|
},
|
|
17354
|
+
usage: emptyUsageSummary(CONTEXT_TREE_SNAPSHOT_WINDOWS.SEVEN_DAYS),
|
|
17014
17355
|
updates: [],
|
|
17015
17356
|
nodes: [],
|
|
17016
17357
|
edges: [],
|
|
@@ -17586,11 +17927,16 @@ async function contextTreeSnapshotRoutes(app) {
|
|
|
17586
17927
|
const orgId = await resolveUserPrimaryOrgId(app.db, userId);
|
|
17587
17928
|
const binding = orgId ? await getOrgContextTree(app.db, orgId) : {};
|
|
17588
17929
|
const githubToken = contextTreeGithubTokenForRepo(binding.repo, app.config.contextTreeSync);
|
|
17930
|
+
const window = query.window ?? "7d";
|
|
17589
17931
|
const snapshot = await getContextTreeSnapshot({
|
|
17590
17932
|
...binding,
|
|
17591
17933
|
githubToken
|
|
17592
|
-
},
|
|
17593
|
-
|
|
17934
|
+
}, window);
|
|
17935
|
+
const usage = orgId ? await summarizeContextTreeUsage(app.db, orgId, contextTreeSnapshotWindowDays(window)) : snapshot.usage;
|
|
17936
|
+
return contextTreeSnapshotSchema.parse({
|
|
17937
|
+
...snapshot,
|
|
17938
|
+
usage
|
|
17939
|
+
});
|
|
17594
17940
|
});
|
|
17595
17941
|
}
|
|
17596
17942
|
function contextTreeGithubTokenForRepo(repo, syncConfig) {
|
|
@@ -17719,7 +18065,7 @@ async function healthzRoutes(app) {
|
|
|
17719
18065
|
* `api/orgs/invitations.ts` (Class B, admin-gated).
|
|
17720
18066
|
*/
|
|
17721
18067
|
async function publicInvitationRoutes(app) {
|
|
17722
|
-
const { previewInvitation } = await import("./invitation-CNv7gfFF-
|
|
18068
|
+
const { previewInvitation } = await import("./invitation-CNv7gfFF-DOFZ75wb.mjs");
|
|
17723
18069
|
app.get("/:token/preview", async (request, reply) => {
|
|
17724
18070
|
if (!request.params.token) throw new UnauthorizedError("Token required");
|
|
17725
18071
|
const preview = await previewInvitation(app.db, request.params.token);
|
|
@@ -17798,6 +18144,7 @@ async function meRoutes(app) {
|
|
|
17798
18144
|
createdAt: m.createdAt
|
|
17799
18145
|
})));
|
|
17800
18146
|
const defaultOrgId = defaultMembership ? memberships.find((m) => m.memberId === defaultMembership.id)?.organizationId ?? null : null;
|
|
18147
|
+
const memberCounts = await countActiveMembersByOrgs(app.db, memberships.map((mb) => mb.organizationId));
|
|
17801
18148
|
let inviteUrl = null;
|
|
17802
18149
|
if (defaultOrgId) {
|
|
17803
18150
|
if (memberships.find((m) => m.organizationId === defaultOrgId)?.role === "admin") {
|
|
@@ -17814,7 +18161,8 @@ async function meRoutes(app) {
|
|
|
17814
18161
|
organizationId: mb.organizationId,
|
|
17815
18162
|
organizationName: mb.orgDisplayName,
|
|
17816
18163
|
role: mb.role,
|
|
17817
|
-
agentId: mb.agentId
|
|
18164
|
+
agentId: mb.agentId,
|
|
18165
|
+
orgHasOtherMembers: (memberCounts.get(mb.organizationId) ?? 1) > 1
|
|
17818
18166
|
})),
|
|
17819
18167
|
onboarding: {
|
|
17820
18168
|
step: onboardingStep,
|
|
@@ -18006,7 +18354,7 @@ async function meRoutes(app) {
|
|
|
18006
18354
|
*/
|
|
18007
18355
|
app.get("/me/pinned-agents", async (request) => {
|
|
18008
18356
|
const { userId } = requireUser(request);
|
|
18009
|
-
const { listMyPinnedAgents } = await import("./client-
|
|
18357
|
+
const { listMyPinnedAgents } = await import("./client-CREn8bJ0-C5fHJir6.mjs");
|
|
18010
18358
|
return listMyPinnedAgents(app.db, { userId });
|
|
18011
18359
|
});
|
|
18012
18360
|
/**
|
|
@@ -18136,6 +18484,80 @@ async function inferOnboardingStep(app, userId) {
|
|
|
18136
18484
|
if (!hasAgent) return "create_agent";
|
|
18137
18485
|
return "completed";
|
|
18138
18486
|
}
|
|
18487
|
+
const MAX_DOC_BYTES = 5 * 1024 * 1024;
|
|
18488
|
+
async function getMeDocPreview(input) {
|
|
18489
|
+
const workspaceRootReal = await realpathOrNotFound(join(input.workspacesRoot ?? join(DEFAULT_DATA_DIR$1, "workspaces"), input.agentName, input.chatId));
|
|
18490
|
+
const candidate = resolve(workspaceRootReal, join(input.basePath ?? "", input.path));
|
|
18491
|
+
if (extname(assertInsideWorkspace(workspaceRootReal, candidate)).toLowerCase() !== ".md") throw new ForbiddenError("Document preview only supports markdown files in the agent workspace");
|
|
18492
|
+
let fileStat;
|
|
18493
|
+
try {
|
|
18494
|
+
fileStat = await stat(candidate);
|
|
18495
|
+
} catch {
|
|
18496
|
+
throw new NotFoundError("Document not found");
|
|
18497
|
+
}
|
|
18498
|
+
if (!fileStat.isFile()) throw new NotFoundError("Document not found");
|
|
18499
|
+
if (fileStat.size > MAX_DOC_BYTES) throw new AppError(413, "Document is larger than the 5MB preview limit");
|
|
18500
|
+
const fileReal = await realpath(candidate);
|
|
18501
|
+
const normalizedPath = assertInsideWorkspace(workspaceRootReal, fileReal);
|
|
18502
|
+
const refPath = normalizeRefPath(input.path);
|
|
18503
|
+
const normalizedBasePath = input.basePath ? normalizeRefPath(input.basePath) : void 0;
|
|
18504
|
+
return {
|
|
18505
|
+
ref: {
|
|
18506
|
+
type: "workspace",
|
|
18507
|
+
chatId: input.chatId,
|
|
18508
|
+
agentId: input.agentId,
|
|
18509
|
+
...normalizedBasePath ? { basePath: normalizedBasePath } : {},
|
|
18510
|
+
path: refPath
|
|
18511
|
+
},
|
|
18512
|
+
path: normalizedPath,
|
|
18513
|
+
content: await readFile(fileReal, "utf8")
|
|
18514
|
+
};
|
|
18515
|
+
}
|
|
18516
|
+
async function realpathOrNotFound(path) {
|
|
18517
|
+
try {
|
|
18518
|
+
return await realpath(path);
|
|
18519
|
+
} catch {
|
|
18520
|
+
throw new NotFoundError("Document not found");
|
|
18521
|
+
}
|
|
18522
|
+
}
|
|
18523
|
+
function assertInsideWorkspace(workspaceRoot, target) {
|
|
18524
|
+
const rel = relative(workspaceRoot, target);
|
|
18525
|
+
if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new ForbiddenError("Document path must stay inside the agent workspace");
|
|
18526
|
+
return rel.split(sep).join("/");
|
|
18527
|
+
}
|
|
18528
|
+
function normalizeRefPath(path) {
|
|
18529
|
+
const parts = [];
|
|
18530
|
+
for (const part of path.split(/[\\/]/)) {
|
|
18531
|
+
if (!part || part === ".") continue;
|
|
18532
|
+
if (part === "..") {
|
|
18533
|
+
if (parts.length === 0) throw new ForbiddenError("Document path must stay inside the agent workspace");
|
|
18534
|
+
parts.pop();
|
|
18535
|
+
continue;
|
|
18536
|
+
}
|
|
18537
|
+
parts.push(part);
|
|
18538
|
+
}
|
|
18539
|
+
if (parts.length === 0) throw new ForbiddenError("Document path must name a markdown file");
|
|
18540
|
+
return parts.join("/");
|
|
18541
|
+
}
|
|
18542
|
+
async function meDocsRoutes(app, options = {}) {
|
|
18543
|
+
app.get("/chats/:chatId/docs/preview", async (request) => {
|
|
18544
|
+
await requireChatAccess(request, app.db);
|
|
18545
|
+
const query = getMeDocSchema.parse(request.query);
|
|
18546
|
+
const [participant] = await app.db.select({ agentId: chatMembership.agentId }).from(chatMembership).where(and(eq(chatMembership.chatId, request.params.chatId), eq(chatMembership.agentId, query.agentId), eq(chatMembership.accessMode, "speaker"))).limit(1);
|
|
18547
|
+
if (!participant) throw new NotFoundError("Document not found");
|
|
18548
|
+
const [agent] = await app.db.select({ name: agents.name }).from(agents).where(eq(agents.uuid, query.agentId)).limit(1);
|
|
18549
|
+
if (!agent?.name) throw new NotFoundError("Document not found");
|
|
18550
|
+
const preview = await getMeDocPreview({
|
|
18551
|
+
chatId: request.params.chatId,
|
|
18552
|
+
agentId: query.agentId,
|
|
18553
|
+
agentName: agent.name,
|
|
18554
|
+
basePath: query.basePath,
|
|
18555
|
+
path: query.path,
|
|
18556
|
+
workspacesRoot: options.workspacesRoot
|
|
18557
|
+
});
|
|
18558
|
+
return getMeDocResponseSchema.parse(preview);
|
|
18559
|
+
});
|
|
18560
|
+
}
|
|
18139
18561
|
/**
|
|
18140
18562
|
* Resolve the caller's active membership in `:orgId` (from the URL) and
|
|
18141
18563
|
* return the full `OrgScope`. The type signature requires
|
|
@@ -18316,7 +18738,7 @@ async function orgAgentRoutes(app) {
|
|
|
18316
18738
|
const { type } = listAgentsFilterSchema.parse(request.query);
|
|
18317
18739
|
const result = await listAgentsForMember(app.db, scope, query.limit, query.cursor, type);
|
|
18318
18740
|
return {
|
|
18319
|
-
items: result.items.map((a) => ({
|
|
18741
|
+
items: result.items.map(({ avatarImageUpdatedAt, ...a }) => ({
|
|
18320
18742
|
...a,
|
|
18321
18743
|
managerId: a.managerId ?? null,
|
|
18322
18744
|
presenceStatus: a.presenceStatus ?? "offline",
|
|
@@ -18325,7 +18747,8 @@ async function orgAgentRoutes(app) {
|
|
|
18325
18747
|
clientId: a.clientId ?? null,
|
|
18326
18748
|
runtimeType: a.runtimeType ?? null,
|
|
18327
18749
|
runtimeState: a.runtimeState ?? null,
|
|
18328
|
-
activeSessions: a.activeSessions ?? null
|
|
18750
|
+
activeSessions: a.activeSessions ?? null,
|
|
18751
|
+
avatarImageUrl: agentAvatarImageUrl(a.uuid, avatarImageUpdatedAt)
|
|
18329
18752
|
})),
|
|
18330
18753
|
nextCursor: result.nextCursor
|
|
18331
18754
|
};
|
|
@@ -18341,7 +18764,7 @@ async function orgAgentRoutes(app) {
|
|
|
18341
18764
|
const query = paginationQuerySchema.parse(request.query);
|
|
18342
18765
|
const result = await listAgentsForAdmin(app.db, scope, query.limit, query.cursor);
|
|
18343
18766
|
return {
|
|
18344
|
-
items: result.items.map((a) => ({
|
|
18767
|
+
items: result.items.map(({ avatarImageUpdatedAt, ...a }) => ({
|
|
18345
18768
|
...a,
|
|
18346
18769
|
managerId: a.managerId ?? null,
|
|
18347
18770
|
presenceStatus: a.presenceStatus ?? "offline",
|
|
@@ -18350,7 +18773,8 @@ async function orgAgentRoutes(app) {
|
|
|
18350
18773
|
clientId: a.clientId ?? null,
|
|
18351
18774
|
runtimeType: a.runtimeType ?? null,
|
|
18352
18775
|
runtimeState: a.runtimeState ?? null,
|
|
18353
|
-
activeSessions: a.activeSessions ?? null
|
|
18776
|
+
activeSessions: a.activeSessions ?? null,
|
|
18777
|
+
avatarImageUrl: agentAvatarImageUrl(a.uuid, avatarImageUpdatedAt)
|
|
18354
18778
|
})),
|
|
18355
18779
|
nextCursor: result.nextCursor
|
|
18356
18780
|
};
|
|
@@ -18443,6 +18867,17 @@ async function orgChatRoutes(app) {
|
|
|
18443
18867
|
return listMeChats(app.db, scope.humanAgentId, scope.organizationId, query);
|
|
18444
18868
|
});
|
|
18445
18869
|
/**
|
|
18870
|
+
* GET /orgs/:orgId/chats/source-counts — per-source aggregate powering the
|
|
18871
|
+
* conversation-list tag bar (Manual / GitHub PR / GitHub Issue / Feishu).
|
|
18872
|
+
* Returns counts only for sources the caller has chats in, plus an
|
|
18873
|
+
* always-present `manual` entry. Same engagement view filter as the list.
|
|
18874
|
+
*/
|
|
18875
|
+
app.get("/source-counts", async (request) => {
|
|
18876
|
+
const scope = await requireOrgMembership(request, app.db);
|
|
18877
|
+
const query = listMeChatSourceCountsQuerySchema.parse(request.query);
|
|
18878
|
+
return listMeChatSourceCounts(app.db, scope.humanAgentId, scope.organizationId, query);
|
|
18879
|
+
});
|
|
18880
|
+
/**
|
|
18446
18881
|
* POST /orgs/:orgId/chats — create a new chat. The :orgId path param
|
|
18447
18882
|
* makes the org explicit; visibility of every requested participant is
|
|
18448
18883
|
* verified before the service layer touches the DB.
|
|
@@ -18496,11 +18931,16 @@ async function orgContextTreeSnapshotRoutes(app) {
|
|
|
18496
18931
|
const scope = await requireOrgMembership(request, app.db);
|
|
18497
18932
|
const binding = await getOrgContextTree(app.db, scope.organizationId);
|
|
18498
18933
|
const githubToken = contextTreeGithubTokenForRepo(binding.repo, app.config.contextTreeSync);
|
|
18934
|
+
const window = query.window ?? "7d";
|
|
18499
18935
|
const snapshot = await getContextTreeSnapshot({
|
|
18500
18936
|
...binding,
|
|
18501
18937
|
githubToken
|
|
18502
|
-
},
|
|
18503
|
-
|
|
18938
|
+
}, window);
|
|
18939
|
+
const usage = await summarizeContextTreeUsage(app.db, scope.organizationId, contextTreeSnapshotWindowDays(window));
|
|
18940
|
+
return contextTreeSnapshotSchema.parse({
|
|
18941
|
+
...snapshot,
|
|
18942
|
+
usage
|
|
18943
|
+
});
|
|
18504
18944
|
});
|
|
18505
18945
|
}
|
|
18506
18946
|
function orgIdParam(params) {
|
|
@@ -19141,6 +19581,64 @@ const githubEntityChatMappings = pgTable("github_entity_chat_mappings", {
|
|
|
19141
19581
|
table.entityType,
|
|
19142
19582
|
table.entityKey
|
|
19143
19583
|
] }), index("idx_github_entity_chat_mappings_chat").on(table.chatId)]);
|
|
19584
|
+
/**
|
|
19585
|
+
* Auto-archive chats when one of their bound pull requests is merged.
|
|
19586
|
+
*
|
|
19587
|
+
* Trigger: GitHub `pull_request.closed` webhook with `merged === true`. The
|
|
19588
|
+
* webhook handler calls this service on a bypass branch — the normalize /
|
|
19589
|
+
* audience / deliver pipeline is unaffected (PR closed events still drop in
|
|
19590
|
+
* Stage 1).
|
|
19591
|
+
*
|
|
19592
|
+
* Algorithm: a merged PR flips every chat bound to it into the user's
|
|
19593
|
+
* archived view, with no inspection of sibling PR state. Multi-PR chats can
|
|
19594
|
+
* be temporarily archived while siblings are still open; any later activity
|
|
19595
|
+
* on those siblings produces a normal delivery message, which the existing
|
|
19596
|
+
* chat-projection auto-revives back to `active`. This trades perfect timing
|
|
19597
|
+
* for zero local state, no GitHub API calls, and no schema changes.
|
|
19598
|
+
*
|
|
19599
|
+
* Per-user safety: writes use an UPSERT guarded with
|
|
19600
|
+
* `setWhere = engagement_status = 'active'`, so only the implicit-active or
|
|
19601
|
+
* explicitly-active rows flip. User-manually `deleted` and already-`archived`
|
|
19602
|
+
* rows are left alone. Idempotent under GitHub webhook retries.
|
|
19603
|
+
*/
|
|
19604
|
+
const { ACTIVE, ARCHIVED } = CHAT_ENGAGEMENT_STATUSES;
|
|
19605
|
+
async function archiveChatsForMergedPr(db, input) {
|
|
19606
|
+
if (!input.repoFullName || !Number.isFinite(input.prNumber) || input.prNumber <= 0) return {
|
|
19607
|
+
chats: 0,
|
|
19608
|
+
rowsConsidered: 0
|
|
19609
|
+
};
|
|
19610
|
+
const entityKey = `${input.repoFullName}#${input.prNumber}`;
|
|
19611
|
+
const rows = await db.select({
|
|
19612
|
+
chatId: githubEntityChatMappings.chatId,
|
|
19613
|
+
humanAgentId: githubEntityChatMappings.humanAgentId
|
|
19614
|
+
}).from(githubEntityChatMappings).where(and(eq(githubEntityChatMappings.organizationId, input.organizationId), eq(githubEntityChatMappings.entityType, "pull_request"), eq(githubEntityChatMappings.entityKey, entityKey)));
|
|
19615
|
+
if (rows.length === 0) return {
|
|
19616
|
+
chats: 0,
|
|
19617
|
+
rowsConsidered: 0
|
|
19618
|
+
};
|
|
19619
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19620
|
+
const targets = [];
|
|
19621
|
+
for (const row of rows) {
|
|
19622
|
+
const key = `${row.chatId}|${row.humanAgentId}`;
|
|
19623
|
+
if (seen.has(key)) continue;
|
|
19624
|
+
seen.add(key);
|
|
19625
|
+
targets.push(row);
|
|
19626
|
+
}
|
|
19627
|
+
for (const { chatId, humanAgentId } of targets) await db.insert(chatUserState).values({
|
|
19628
|
+
chatId,
|
|
19629
|
+
agentId: humanAgentId,
|
|
19630
|
+
unreadMentionCount: 0,
|
|
19631
|
+
engagementStatus: ARCHIVED
|
|
19632
|
+
}).onConflictDoUpdate({
|
|
19633
|
+
target: [chatUserState.chatId, chatUserState.agentId],
|
|
19634
|
+
set: { engagementStatus: ARCHIVED },
|
|
19635
|
+
setWhere: eq(chatUserState.engagementStatus, ACTIVE)
|
|
19636
|
+
});
|
|
19637
|
+
return {
|
|
19638
|
+
chats: new Set(targets.map((t) => t.chatId)).size,
|
|
19639
|
+
rowsConsidered: targets.length
|
|
19640
|
+
};
|
|
19641
|
+
}
|
|
19144
19642
|
function evaluateDelegateTarget(target, sourceOrgId) {
|
|
19145
19643
|
if (!target) return "not_found";
|
|
19146
19644
|
if (target.organizationId !== sourceOrgId) return "cross_org";
|
|
@@ -20286,6 +20784,28 @@ async function githubAppWebhookRoutes(app) {
|
|
|
20286
20784
|
};
|
|
20287
20785
|
const deliveryHeader = request.headers["x-github-delivery"];
|
|
20288
20786
|
const deliveryId = typeof deliveryHeader === "string" && deliveryHeader.length > 0 ? deliveryHeader : null;
|
|
20787
|
+
if (eventType === "pull_request" && isRecord(payload) && payload.action === "closed") {
|
|
20788
|
+
const pr = isRecord(payload.pull_request) ? payload.pull_request : null;
|
|
20789
|
+
const repoFullName = readString$1((isRecord(payload.repository) ? payload.repository : null)?.full_name);
|
|
20790
|
+
const prNumber = readNumber$1(pr?.number);
|
|
20791
|
+
if (pr?.merged === true && repoFullName && prNumber !== null) try {
|
|
20792
|
+
const stats = await archiveChatsForMergedPr(app.db, {
|
|
20793
|
+
organizationId: installation.hubOrganizationId,
|
|
20794
|
+
repoFullName,
|
|
20795
|
+
prNumber
|
|
20796
|
+
});
|
|
20797
|
+
log$1.info({
|
|
20798
|
+
entityKey: `${repoFullName}#${prNumber}`,
|
|
20799
|
+
...stats
|
|
20800
|
+
}, "auto-archived chats on PR merge");
|
|
20801
|
+
} catch (err) {
|
|
20802
|
+
log$1.error({
|
|
20803
|
+
err,
|
|
20804
|
+
repoFullName,
|
|
20805
|
+
prNumber
|
|
20806
|
+
}, "failed to auto-archive chats on PR merge");
|
|
20807
|
+
}
|
|
20808
|
+
}
|
|
20289
20809
|
const event = normalizeGithubEvent(eventType, payload, source, deliveryId);
|
|
20290
20810
|
if (!event) {
|
|
20291
20811
|
log$1.debug({
|
|
@@ -21886,12 +22406,14 @@ async function buildApp(config) {
|
|
|
21886
22406
|
await api.register(githubOauthRoutes, { prefix: "/auth/github" });
|
|
21887
22407
|
await api.register(publicInvitationRoutes, { prefix: "/invitations" });
|
|
21888
22408
|
await api.register(bootstrapConfigRoutes, { prefix: "/bootstrap" });
|
|
22409
|
+
await api.register(publicAgentAvatarRoutes, { prefix: "/agents" });
|
|
21889
22410
|
await api.register(userScope("contextTreeScope", async (scope) => {
|
|
21890
22411
|
await scope.register(contextTreeInfoRoutes);
|
|
21891
22412
|
await scope.register(contextTreeSnapshotRoutes);
|
|
21892
22413
|
}), { prefix: "/context-tree" });
|
|
21893
22414
|
await api.register(userScope("meRoutesScope", async (scope) => {
|
|
21894
22415
|
await scope.register(meRoutes);
|
|
22416
|
+
await scope.register(meDocsRoutes, { workspacesRoot: config.workspace.root });
|
|
21895
22417
|
}), { prefix: "" });
|
|
21896
22418
|
await api.register(userScope("orgsScope", async (scope) => {
|
|
21897
22419
|
await scope.register(orgIdentityRoutes);
|