@agent-team-foundation/first-tree-hub 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{bootstrap-Cya2OoHz.mjs → bootstrap-C15ZBOCC.mjs} +1 -0
- package/dist/cli/index.mjs +7 -7
- package/dist/{client-BH4CmUL0-CybE3kuP.mjs → client-RM_03B_l-DiEIa9xe.mjs} +14 -3
- package/dist/{client-h4KZ3b9o-CQyibXig.mjs → client-gSnsRu5W-v_mC1sRY.mjs} +2 -2
- 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-DX3-nDs9.mjs} +613 -67
- package/dist/web/assets/index-BOK7e_td.css +1 -0
- package/dist/web/assets/{index-CJcRUZ8l.js → index-BppGbmdT.js} +1 -1
- package/dist/web/assets/index-Is-7_y6O.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-RM_03B_l-DiEIa9xe.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",
|
|
@@ -4691,6 +4774,7 @@ const createClaudeCodeHandler = (config) => {
|
|
|
4691
4774
|
/** Worktrees materialised for this session — each entry removed on shutdown. */
|
|
4692
4775
|
const ownedWorktrees = [];
|
|
4693
4776
|
async function toSDKUserMessage(message, sessionCtx, sessionId) {
|
|
4777
|
+
emitContextTreeUsage(sessionCtx);
|
|
4694
4778
|
if (message.format === "file") {
|
|
4695
4779
|
const senderLabel = message.senderId ? await sessionCtx.resolveSenderLabel(message.senderId) : "";
|
|
4696
4780
|
const prefix = senderLabel ? `[From: ${senderLabel}]\n\n` : "";
|
|
@@ -4753,6 +4837,16 @@ const createClaudeCodeHandler = (config) => {
|
|
|
4753
4837
|
session_id: sessionId
|
|
4754
4838
|
};
|
|
4755
4839
|
}
|
|
4840
|
+
function emitContextTreeUsage(sessionCtx) {
|
|
4841
|
+
if (!contextTreePath) return;
|
|
4842
|
+
sessionCtx.emitEvent({
|
|
4843
|
+
kind: "context_tree_usage",
|
|
4844
|
+
payload: {
|
|
4845
|
+
purpose: "design_decision",
|
|
4846
|
+
treeRepoUrl: contextTreeRepoUrl
|
|
4847
|
+
}
|
|
4848
|
+
});
|
|
4849
|
+
}
|
|
4756
4850
|
/**
|
|
4757
4851
|
* Build env for the child Claude Code process.
|
|
4758
4852
|
*
|
|
@@ -5356,6 +5450,16 @@ const createCodexHandler = (config) => {
|
|
|
5356
5450
|
function toCodexInput(message, sessionCtx) {
|
|
5357
5451
|
return sessionCtx.formatInboundContent(message).then((text) => text);
|
|
5358
5452
|
}
|
|
5453
|
+
function emitContextTreeUsage(sessionCtx) {
|
|
5454
|
+
if (!contextTreePath) return;
|
|
5455
|
+
sessionCtx.emitEvent({
|
|
5456
|
+
kind: "context_tree_usage",
|
|
5457
|
+
payload: {
|
|
5458
|
+
purpose: "design_decision",
|
|
5459
|
+
treeRepoUrl: contextTreeRepoUrl
|
|
5460
|
+
}
|
|
5461
|
+
});
|
|
5462
|
+
}
|
|
5359
5463
|
async function prepareGitWorktrees(payload, workspaceCwd, sessionCtx) {
|
|
5360
5464
|
if (!gitMirrorManager) return;
|
|
5361
5465
|
const branchAgentKey = agentName ?? sessionCtx.agent.agentId;
|
|
@@ -5484,6 +5588,7 @@ const createCodexHandler = (config) => {
|
|
|
5484
5588
|
if (!activeThread) return;
|
|
5485
5589
|
const abort = new AbortController();
|
|
5486
5590
|
currentAbort = abort;
|
|
5591
|
+
emitContextTreeUsage(sessionCtx);
|
|
5487
5592
|
sessionCtx.setRuntimeState("working");
|
|
5488
5593
|
const assistantTexts = [];
|
|
5489
5594
|
let turnFailed = false;
|
|
@@ -5968,13 +6073,17 @@ var Deduplicator = class {
|
|
|
5968
6073
|
};
|
|
5969
6074
|
function createResultSink(deps) {
|
|
5970
6075
|
async function buildMetadata(trigger) {
|
|
5971
|
-
|
|
5972
|
-
const
|
|
5973
|
-
if (
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
6076
|
+
const metadata = {};
|
|
6077
|
+
const documentBasePath = await deps.getDocumentBasePath?.();
|
|
6078
|
+
if (documentBasePath) metadata.documentContext = documentContextSchema.parse({ basePath: documentBasePath });
|
|
6079
|
+
if (trigger && trigger.senderId !== deps.agent.agentId) {
|
|
6080
|
+
const participants = await deps.participants.get();
|
|
6081
|
+
if (participants.length <= 2) {
|
|
6082
|
+
const peer = participants.find((p) => p.agentId === trigger.senderId);
|
|
6083
|
+
if (!peer || peer.mode === "mention_only") metadata.mentions = [trigger.senderId];
|
|
6084
|
+
} else metadata.mentions = [trigger.senderId];
|
|
6085
|
+
}
|
|
6086
|
+
return Object.keys(metadata).length > 0 ? metadata : void 0;
|
|
5978
6087
|
}
|
|
5979
6088
|
return async function forwardResult(text) {
|
|
5980
6089
|
if (text.trim().length === 0) {
|
|
@@ -6065,6 +6174,16 @@ var SessionRegistry = class {
|
|
|
6065
6174
|
}
|
|
6066
6175
|
}
|
|
6067
6176
|
};
|
|
6177
|
+
function documentBasePathFromRuntimeConfig(payload) {
|
|
6178
|
+
if (payload.gitRepos.length !== 1) return null;
|
|
6179
|
+
const repo = payload.gitRepos[0];
|
|
6180
|
+
if (!repo) return null;
|
|
6181
|
+
const localPath = repoLocalPath(repo).trim();
|
|
6182
|
+
return localPath.length > 0 ? localPath : null;
|
|
6183
|
+
}
|
|
6184
|
+
function repoLocalPath(repo) {
|
|
6185
|
+
return repo.localPath ?? deriveRepoLocalPath(repo.url);
|
|
6186
|
+
}
|
|
6068
6187
|
/**
|
|
6069
6188
|
* Manages per-chat session entries with session-oriented handler lifecycle.
|
|
6070
6189
|
*
|
|
@@ -6554,7 +6673,8 @@ var SessionManager = class {
|
|
|
6554
6673
|
this.currentTrigger.delete(chatId);
|
|
6555
6674
|
},
|
|
6556
6675
|
log,
|
|
6557
|
-
participants
|
|
6676
|
+
participants,
|
|
6677
|
+
getDocumentBasePath: () => this.resolveDocumentBasePath(log)
|
|
6558
6678
|
});
|
|
6559
6679
|
const envCtx = {
|
|
6560
6680
|
sdk: this.config.sdk,
|
|
@@ -6582,6 +6702,16 @@ var SessionManager = class {
|
|
|
6582
6702
|
resolveSenderLabel: async (senderId) => resolveSenderLabel(senderId, await participants.get())
|
|
6583
6703
|
};
|
|
6584
6704
|
}
|
|
6705
|
+
async resolveDocumentBasePath(log) {
|
|
6706
|
+
if (!this.config.agentConfigCache) return null;
|
|
6707
|
+
try {
|
|
6708
|
+
const { payload } = await this.config.agentConfigCache.refreshIfNewer(this.config.agentIdentity.agentId, 0);
|
|
6709
|
+
return documentBasePathFromRuntimeConfig(payload);
|
|
6710
|
+
} catch (err) {
|
|
6711
|
+
log(`document preview base path unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
6712
|
+
return null;
|
|
6713
|
+
}
|
|
6714
|
+
}
|
|
6585
6715
|
/** Update per-session runtime state and recompute aggregate. Only active sessions may update. */
|
|
6586
6716
|
setSessionRuntimeState(chatId, state) {
|
|
6587
6717
|
const session = this.sessions.get(chatId);
|
|
@@ -9553,7 +9683,7 @@ async function onboardCreate(args) {
|
|
|
9553
9683
|
}
|
|
9554
9684
|
const runtimeAgent = args.type === "human" ? args.assistant : args.id;
|
|
9555
9685
|
if (args.feishuBotAppId && args.feishuBotAppSecret) {
|
|
9556
|
-
const { bindFeishuBot } = await import("./feishu-
|
|
9686
|
+
const { bindFeishuBot } = await import("./feishu-BGx71p5s.mjs").then((n) => n.r);
|
|
9557
9687
|
const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
|
|
9558
9688
|
if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
|
|
9559
9689
|
else {
|
|
@@ -10766,7 +10896,7 @@ function createFeedbackHandler(config) {
|
|
|
10766
10896
|
return { handle };
|
|
10767
10897
|
}
|
|
10768
10898
|
//#endregion
|
|
10769
|
-
//#region ../server/dist/app-
|
|
10899
|
+
//#region ../server/dist/app-l2iy80P2.mjs
|
|
10770
10900
|
var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
|
|
10771
10901
|
init_esm();
|
|
10772
10902
|
var __defProp = Object.defineProperty;
|
|
@@ -11376,6 +11506,21 @@ async function ensureDefaultOrganization(db) {
|
|
|
11376
11506
|
*/
|
|
11377
11507
|
const RESERVED_AGENT_NAME_PREFIX = "__";
|
|
11378
11508
|
/**
|
|
11509
|
+
* Derive the relative URL clients should use to fetch a manager-uploaded
|
|
11510
|
+
* avatar image. Returns `null` when no image is set. Embeds the upload
|
|
11511
|
+
* timestamp as `?v=<epoch>` so a fresh upload busts any browser cache
|
|
11512
|
+
* that may have memoised the previous version.
|
|
11513
|
+
*
|
|
11514
|
+
* Auth: the image route is intentionally public read — the URL leaks no
|
|
11515
|
+
* more than the agent's UUID, which is already required to address it.
|
|
11516
|
+
* Keeping it unauthenticated lets `<img src>` render without bespoke
|
|
11517
|
+
* fetch-and-blob plumbing.
|
|
11518
|
+
*/
|
|
11519
|
+
function agentAvatarImageUrl(uuid, updatedAt) {
|
|
11520
|
+
if (!updatedAt) return null;
|
|
11521
|
+
return `/api/v1/agents/${uuid}/avatar?v=${updatedAt.getTime()}`;
|
|
11522
|
+
}
|
|
11523
|
+
/**
|
|
11379
11524
|
* True iff `clients.metadata.capabilities` is a non-empty object — i.e. the
|
|
11380
11525
|
* client has reported at least one runtime probe result. Used to distinguish
|
|
11381
11526
|
* "we don't know what's installed yet" (empty / never reported) from
|
|
@@ -11634,6 +11779,8 @@ async function listAgentsForAdmin(db, scope, limit, cursor) {
|
|
|
11634
11779
|
managerId: agents.managerId,
|
|
11635
11780
|
clientId: agents.clientId,
|
|
11636
11781
|
runtimeProvider: agents.runtimeProvider,
|
|
11782
|
+
avatarColorToken: agents.avatarColorToken,
|
|
11783
|
+
avatarImageUpdatedAt: agents.avatarImageUpdatedAt,
|
|
11637
11784
|
createdAt: agents.createdAt,
|
|
11638
11785
|
updatedAt: agents.updatedAt,
|
|
11639
11786
|
presenceStatus: agentPresence.status,
|
|
@@ -11672,6 +11819,8 @@ async function listAgentsForMember(db, scope, limit, cursor, type) {
|
|
|
11672
11819
|
managerId: agents.managerId,
|
|
11673
11820
|
clientId: agents.clientId,
|
|
11674
11821
|
runtimeProvider: agents.runtimeProvider,
|
|
11822
|
+
avatarColorToken: agents.avatarColorToken,
|
|
11823
|
+
avatarImageUpdatedAt: agents.avatarImageUpdatedAt,
|
|
11675
11824
|
createdAt: agents.createdAt,
|
|
11676
11825
|
updatedAt: agents.updatedAt,
|
|
11677
11826
|
presenceStatus: agentPresence.status,
|
|
@@ -11708,6 +11857,7 @@ async function updateAgent(db, uuid, data) {
|
|
|
11708
11857
|
}
|
|
11709
11858
|
if (data.visibility !== void 0) updates.visibility = data.visibility;
|
|
11710
11859
|
if (data.metadata !== void 0) updates.metadata = data.metadata;
|
|
11860
|
+
if (data.avatarColorToken !== void 0) updates.avatarColorToken = data.avatarColorToken;
|
|
11711
11861
|
if (data.managerId !== void 0) {
|
|
11712
11862
|
if (data.managerId === null) throw new BadRequestError("managerId cannot be cleared — every agent must have a manager");
|
|
11713
11863
|
const [manager] = await db.select({
|
|
@@ -11814,6 +11964,63 @@ async function deleteAgent(db, uuid) {
|
|
|
11814
11964
|
if (!agent) throw new Error("Unexpected: UPDATE RETURNING produced no row");
|
|
11815
11965
|
return agent;
|
|
11816
11966
|
}
|
|
11967
|
+
/**
|
|
11968
|
+
* Supported avatar-image MIME types. The web client always uploads WEBP after
|
|
11969
|
+
* its own resize step; we accept PNG/JPEG too so a caller using the raw HTTP
|
|
11970
|
+
* API (curl, scripts) doesn't have to re-encode. Anything else is rejected at
|
|
11971
|
+
* the boundary — we never store an unknown content type.
|
|
11972
|
+
*/
|
|
11973
|
+
const SUPPORTED_AVATAR_IMAGE_MIMES = [
|
|
11974
|
+
"image/webp",
|
|
11975
|
+
"image/png",
|
|
11976
|
+
"image/jpeg"
|
|
11977
|
+
];
|
|
11978
|
+
/** Hard server-side ceiling for the stored bytea blob. Client pre-resizes to ~50KB. */
|
|
11979
|
+
const MAX_AVATAR_IMAGE_BYTES = 512 * 1024;
|
|
11980
|
+
function isSupportedAvatarMime(mime) {
|
|
11981
|
+
return SUPPORTED_AVATAR_IMAGE_MIMES.find((m) => m === mime) !== void 0;
|
|
11982
|
+
}
|
|
11983
|
+
/**
|
|
11984
|
+
* Fetch the avatar image blob for an agent. Returns `null` when no image
|
|
11985
|
+
* is set (the column is NULL). The data + mime pair is always coherent
|
|
11986
|
+
* (set/cleared together by the service writes below).
|
|
11987
|
+
*/
|
|
11988
|
+
async function getAgentAvatarImage(db, uuid) {
|
|
11989
|
+
const [row] = await db.select({
|
|
11990
|
+
data: agents.avatarImageData,
|
|
11991
|
+
mime: agents.avatarImageMime,
|
|
11992
|
+
updatedAt: agents.avatarImageUpdatedAt
|
|
11993
|
+
}).from(agents).where(and(eq(agents.uuid, uuid), ne(agents.status, AGENT_STATUSES.DELETED))).limit(1);
|
|
11994
|
+
if (!row || !row.data || !row.mime || !row.updatedAt) return null;
|
|
11995
|
+
return {
|
|
11996
|
+
data: row.data,
|
|
11997
|
+
mime: row.mime,
|
|
11998
|
+
updatedAt: row.updatedAt
|
|
11999
|
+
};
|
|
12000
|
+
}
|
|
12001
|
+
/** Replace (or set) an agent's avatar image. Validates mime + size. */
|
|
12002
|
+
async function setAgentAvatarImage(db, uuid, data, mime) {
|
|
12003
|
+
if (!isSupportedAvatarMime(mime)) throw new BadRequestError(`Unsupported avatar image type "${mime}". Use PNG, JPEG, or WEBP.`);
|
|
12004
|
+
if (data.length === 0) throw new BadRequestError("Avatar image payload is empty.");
|
|
12005
|
+
if (data.length > 524288) throw new BadRequestError(`Avatar image is too large (${data.length} bytes; max ${MAX_AVATAR_IMAGE_BYTES}).`);
|
|
12006
|
+
const now = /* @__PURE__ */ new Date();
|
|
12007
|
+
if ((await db.update(agents).set({
|
|
12008
|
+
avatarImageData: data,
|
|
12009
|
+
avatarImageMime: mime,
|
|
12010
|
+
avatarImageUpdatedAt: now,
|
|
12011
|
+
updatedAt: now
|
|
12012
|
+
}).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`);
|
|
12013
|
+
return now;
|
|
12014
|
+
}
|
|
12015
|
+
/** Clear an agent's avatar image (falls back to color + initial). */
|
|
12016
|
+
async function clearAgentAvatarImage(db, uuid) {
|
|
12017
|
+
if ((await db.update(agents).set({
|
|
12018
|
+
avatarImageData: null,
|
|
12019
|
+
avatarImageMime: null,
|
|
12020
|
+
avatarImageUpdatedAt: null,
|
|
12021
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
12022
|
+
}).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`);
|
|
12023
|
+
}
|
|
11817
12024
|
const log$5 = createLogger$1("AgentFeishuBot");
|
|
11818
12025
|
async function agentFeishuBotRoutes(app) {
|
|
11819
12026
|
/**
|
|
@@ -13122,6 +13329,18 @@ async function listEvents(db, agentId, chatId, options) {
|
|
|
13122
13329
|
async function clearEvents(db, agentId, chatId) {
|
|
13123
13330
|
await db.delete(sessionEvents).where(and(eq(sessionEvents.agentId, agentId), eq(sessionEvents.chatId, chatId)));
|
|
13124
13331
|
}
|
|
13332
|
+
async function summarizeContextTreeUsage(db, organizationId, windowDays) {
|
|
13333
|
+
const since = /* @__PURE__ */ new Date(Date.now() - windowDays * 24 * 60 * 60 * 1e3);
|
|
13334
|
+
const [row] = await db.select({
|
|
13335
|
+
agentCount: sql`count(distinct ${sessionEvents.agentId})::int`,
|
|
13336
|
+
usageCount: sql`count(*)::int`
|
|
13337
|
+
}).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)));
|
|
13338
|
+
return {
|
|
13339
|
+
windowDays,
|
|
13340
|
+
agentCount: row?.agentCount ?? 0,
|
|
13341
|
+
usageCount: row?.usageCount ?? 0
|
|
13342
|
+
};
|
|
13343
|
+
}
|
|
13125
13344
|
/**
|
|
13126
13345
|
* Default per-agent in-flight cap when `server.inbox.maxInFlightPerAgent` is
|
|
13127
13346
|
* unset. Mirrors the schema default so a hub running without an explicit
|
|
@@ -13772,6 +13991,22 @@ async function agentActivityRoutes(app) {
|
|
|
13772
13991
|
});
|
|
13773
13992
|
}
|
|
13774
13993
|
/**
|
|
13994
|
+
* Project a DB agent row into its wire shape. Strips the inline image
|
|
13995
|
+
* `avatarImageData` (large bytea, only meant for the image-serve route)
|
|
13996
|
+
* and synthesises the public `avatarImageUrl` from the upload timestamp.
|
|
13997
|
+
* `createdAt`/`updatedAt` are coerced to ISO strings so the response is
|
|
13998
|
+
* pure JSON.
|
|
13999
|
+
*/
|
|
14000
|
+
function serializeAgent(agent) {
|
|
14001
|
+
const { avatarImageData: _data, avatarImageMime: _mime, avatarImageUpdatedAt, createdAt, updatedAt, ...rest } = agent;
|
|
14002
|
+
return {
|
|
14003
|
+
...rest,
|
|
14004
|
+
createdAt: createdAt.toISOString(),
|
|
14005
|
+
updatedAt: updatedAt.toISOString(),
|
|
14006
|
+
avatarImageUrl: agentAvatarImageUrl(agent.uuid, avatarImageUpdatedAt ?? null)
|
|
14007
|
+
};
|
|
14008
|
+
}
|
|
14009
|
+
/**
|
|
13775
14010
|
* Class C — resource-scoped per-agent routes. Mounted at
|
|
13776
14011
|
* `/api/v1/agents/:uuid/...`. The agent's UUID locates its org
|
|
13777
14012
|
* intrinsically; `requireAgentAccess` resolves the caller's membership in
|
|
@@ -13800,11 +14035,7 @@ async function agentRoutes(app) {
|
|
|
13800
14035
|
}
|
|
13801
14036
|
app.get("/:uuid", async (request) => {
|
|
13802
14037
|
const { agent } = await requireAgentAccess(request, app.db, "visible");
|
|
13803
|
-
return
|
|
13804
|
-
...agent,
|
|
13805
|
-
createdAt: agent.createdAt.toISOString(),
|
|
13806
|
-
updatedAt: agent.updatedAt.toISOString()
|
|
13807
|
-
};
|
|
14038
|
+
return serializeAgent(agent);
|
|
13808
14039
|
});
|
|
13809
14040
|
app.patch("/:uuid", { config: { otelRecordBody: true } }, async (request) => {
|
|
13810
14041
|
const { scope } = await requireAgentAccess(request, app.db, "manage");
|
|
@@ -13813,22 +14044,14 @@ async function agentRoutes(app) {
|
|
|
13813
14044
|
const before = body.clientId !== void 0 ? await getAgent(app.db, request.params.uuid) : null;
|
|
13814
14045
|
const agent = await updateAgent(app.db, request.params.uuid, body);
|
|
13815
14046
|
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
|
-
};
|
|
14047
|
+
return serializeAgent(agent);
|
|
13821
14048
|
});
|
|
13822
14049
|
app.patch("/:uuid/rebind", { config: { otelRecordBody: true } }, async (request) => {
|
|
13823
14050
|
await requireAgentAccess(request, app.db, "manage");
|
|
13824
14051
|
const body = rebindAgentSchema.parse(request.body);
|
|
13825
14052
|
const agent = await rebindAgent(app.db, request.params.uuid, body);
|
|
13826
14053
|
notifyClientAgentPinned(agent);
|
|
13827
|
-
return
|
|
13828
|
-
...agent,
|
|
13829
|
-
createdAt: agent.createdAt.toISOString(),
|
|
13830
|
-
updatedAt: agent.updatedAt.toISOString()
|
|
13831
|
-
};
|
|
14054
|
+
return serializeAgent(agent);
|
|
13832
14055
|
});
|
|
13833
14056
|
app.post("/:uuid/disconnect", async (request, reply) => {
|
|
13834
14057
|
await requireAgentAccess(request, app.db, "manage");
|
|
@@ -13838,27 +14061,35 @@ async function agentRoutes(app) {
|
|
|
13838
14061
|
});
|
|
13839
14062
|
app.post("/:uuid/suspend", async (request) => {
|
|
13840
14063
|
await requireAgentAccess(request, app.db, "manage");
|
|
13841
|
-
|
|
13842
|
-
return {
|
|
13843
|
-
...agent,
|
|
13844
|
-
createdAt: agent.createdAt.toISOString(),
|
|
13845
|
-
updatedAt: agent.updatedAt.toISOString()
|
|
13846
|
-
};
|
|
14064
|
+
return serializeAgent(await suspendAgent(app.db, request.params.uuid));
|
|
13847
14065
|
});
|
|
13848
14066
|
app.post("/:uuid/reactivate", async (request) => {
|
|
13849
14067
|
await requireAgentAccess(request, app.db, "manage");
|
|
13850
|
-
|
|
13851
|
-
return {
|
|
13852
|
-
...agent,
|
|
13853
|
-
createdAt: agent.createdAt.toISOString(),
|
|
13854
|
-
updatedAt: agent.updatedAt.toISOString()
|
|
13855
|
-
};
|
|
14068
|
+
return serializeAgent(await reactivateAgent(app.db, request.params.uuid));
|
|
13856
14069
|
});
|
|
13857
14070
|
app.delete("/:uuid", async (request, reply) => {
|
|
13858
14071
|
await requireAgentAccess(request, app.db, "manage");
|
|
13859
14072
|
await deleteAgent(app.db, request.params.uuid);
|
|
13860
14073
|
return reply.status(204).send();
|
|
13861
14074
|
});
|
|
14075
|
+
app.addContentTypeParser(/^image\//, { parseAs: "buffer" }, (_req, body, done) => {
|
|
14076
|
+
done(null, body);
|
|
14077
|
+
});
|
|
14078
|
+
app.put("/:uuid/avatar", { bodyLimit: MAX_AVATAR_IMAGE_BYTES + 1024 }, async (request, reply) => {
|
|
14079
|
+
await requireAgentAccess(request, app.db, "manage");
|
|
14080
|
+
const contentType = request.headers["content-type"];
|
|
14081
|
+
if (typeof contentType !== "string" || !contentType.startsWith("image/")) throw new BadRequestError(`Avatar upload requires an image/* Content-Type. Supported: ${SUPPORTED_AVATAR_IMAGE_MIMES.join(", ")}.`);
|
|
14082
|
+
const mime = contentType.split(";")[0]?.trim() ?? "";
|
|
14083
|
+
const body = request.body;
|
|
14084
|
+
if (!Buffer.isBuffer(body)) throw new BadRequestError("Avatar upload body must be raw image bytes.");
|
|
14085
|
+
const updatedAt = await setAgentAvatarImage(app.db, request.params.uuid, body, mime);
|
|
14086
|
+
return reply.status(200).send({ avatarImageUrl: agentAvatarImageUrl(request.params.uuid, updatedAt) });
|
|
14087
|
+
});
|
|
14088
|
+
app.delete("/:uuid/avatar", async (request, reply) => {
|
|
14089
|
+
await requireAgentAccess(request, app.db, "manage");
|
|
14090
|
+
await clearAgentAvatarImage(app.db, request.params.uuid);
|
|
14091
|
+
return reply.status(204).send();
|
|
14092
|
+
});
|
|
13862
14093
|
app.post("/:uuid/test", async (request, reply) => {
|
|
13863
14094
|
const { uuid } = request.params;
|
|
13864
14095
|
const { agent: targetAgent } = await requireAgentAccess(request, app.db, "manage");
|
|
@@ -13967,6 +14198,23 @@ async function agentRoutes(app) {
|
|
|
13967
14198
|
});
|
|
13968
14199
|
}
|
|
13969
14200
|
/**
|
|
14201
|
+
* Public read-only route for agent avatar images. Mounted outside the
|
|
14202
|
+
* member-JWT auth scope so `<img src>` works without bespoke fetch-and-blob
|
|
14203
|
+
* plumbing. Reading an avatar leaks no more than the agent's UUID — which
|
|
14204
|
+
* is already required to address the route — and the UUID itself is only
|
|
14205
|
+
* exposed through authenticated agent-list calls.
|
|
14206
|
+
*/
|
|
14207
|
+
async function publicAgentAvatarRoutes(app) {
|
|
14208
|
+
app.get("/:uuid/avatar", async (request, reply) => {
|
|
14209
|
+
const image = await getAgentAvatarImage(app.db, request.params.uuid);
|
|
14210
|
+
if (!image) return reply.status(404).send({ error: "Avatar not set" });
|
|
14211
|
+
reply.header("Content-Type", image.mime);
|
|
14212
|
+
reply.header("Cache-Control", "public, max-age=2592000, immutable");
|
|
14213
|
+
reply.header("ETag", `"${image.updatedAt.getTime()}"`);
|
|
14214
|
+
return reply.send(image.data);
|
|
14215
|
+
});
|
|
14216
|
+
}
|
|
14217
|
+
/**
|
|
13970
14218
|
* Class C — `/api/v1/agents/:uuid/config`. Runtime config (system prompt,
|
|
13971
14219
|
* tools, env) is behavior-sensitive — gated on `manage`, not `visible`.
|
|
13972
14220
|
*/
|
|
@@ -15012,6 +15260,22 @@ async function listActiveMemberships(db, userId) {
|
|
|
15012
15260
|
}).from(members).innerJoin(organizations, eq(members.organizationId, organizations.id)).where(and(eq(members.userId, userId), eq(members.status, "active"))).orderBy(desc(members.createdAt));
|
|
15013
15261
|
}
|
|
15014
15262
|
/**
|
|
15263
|
+
* Count ACTIVE members per org, restricted to the given org IDs. Returns a
|
|
15264
|
+
* Map keyed by `organizationId`; orgs absent from the result simply have
|
|
15265
|
+
* zero active members (shouldn't happen in practice — the caller always
|
|
15266
|
+
* passes orgs the user is a member of — but the Map shape lets callers do
|
|
15267
|
+
* `counts.get(orgId) ?? 0` defensively). Used by `/me` to surface
|
|
15268
|
+
* `orgHasOtherMembers` per membership without N+1 queries.
|
|
15269
|
+
*/
|
|
15270
|
+
async function countActiveMembersByOrgs(db, organizationIds) {
|
|
15271
|
+
if (organizationIds.length === 0) return /* @__PURE__ */ new Map();
|
|
15272
|
+
const rows = await db.select({
|
|
15273
|
+
organizationId: members.organizationId,
|
|
15274
|
+
count: sql`count(*)::int`
|
|
15275
|
+
}).from(members).where(and(inArray(members.organizationId, organizationIds), eq(members.status, "active"))).groupBy(members.organizationId);
|
|
15276
|
+
return new Map(rows.map((r) => [r.organizationId, r.count]));
|
|
15277
|
+
}
|
|
15278
|
+
/**
|
|
15015
15279
|
* Pick the most recently joined active membership — used after OAuth login
|
|
15016
15280
|
* when the user already has at least one team but no `next` was specified.
|
|
15017
15281
|
*/
|
|
@@ -15803,16 +16067,16 @@ function decodeCursor(cursor) {
|
|
|
15803
16067
|
return null;
|
|
15804
16068
|
}
|
|
15805
16069
|
}
|
|
15806
|
-
const { ACTIVE, ARCHIVED, DELETED } = CHAT_ENGAGEMENT_STATUSES;
|
|
16070
|
+
const { ACTIVE: ACTIVE$1, ARCHIVED: ARCHIVED$1, DELETED } = CHAT_ENGAGEMENT_STATUSES;
|
|
15807
16071
|
/**
|
|
15808
16072
|
* SQL predicate for each engagement view tab. `deleted` is never a valid view
|
|
15809
16073
|
* value — deleted rows are reachable only through `GET /chats/:chatId` + the
|
|
15810
16074
|
* Restore banner on the chat detail page.
|
|
15811
16075
|
*/
|
|
15812
16076
|
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})`
|
|
16077
|
+
active: sql`COALESCE(cus.engagement_status, ${ACTIVE$1}) = ${ACTIVE$1}`,
|
|
16078
|
+
archived: sql`COALESCE(cus.engagement_status, ${ACTIVE$1}) = ${ARCHIVED$1}`,
|
|
16079
|
+
all: sql`COALESCE(cus.engagement_status, ${ACTIVE$1}) IN (${ACTIVE$1}, ${ARCHIVED$1})`
|
|
15816
16080
|
};
|
|
15817
16081
|
/**
|
|
15818
16082
|
* Write the caller's engagement state for this chat. UPSERT into
|
|
@@ -15840,7 +16104,26 @@ async function setChatEngagement(db, chatId, agentId, status) {
|
|
|
15840
16104
|
*/
|
|
15841
16105
|
async function getCallerEngagement(db, chatId, agentId) {
|
|
15842
16106
|
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;
|
|
16107
|
+
return row?.engagementStatus ?? ACTIVE$1;
|
|
16108
|
+
}
|
|
16109
|
+
const KNOWN_NON_MANUAL_PREDICATE = sql`(
|
|
16110
|
+
(c.metadata->>'source' = 'github' AND c.metadata->>'entityType' IN (${sql.join(GITHUB_ENTITY_TYPES.map((t) => sql`${t}`), sql.raw(", "))}))
|
|
16111
|
+
OR c.metadata->>'source' = 'feishu'
|
|
16112
|
+
)`;
|
|
16113
|
+
const chatSourceSqlExpression = sql`CASE
|
|
16114
|
+
${sql.join(GITHUB_ENTITY_TYPES.map((t) => sql`WHEN c.metadata->>'source' = 'github' AND c.metadata->>'entityType' = ${t} THEN ${`github_${t}`}`), sql.raw("\n "))}
|
|
16115
|
+
WHEN c.metadata->>'source' = 'feishu' THEN 'feishu'
|
|
16116
|
+
ELSE 'manual'
|
|
16117
|
+
END`;
|
|
16118
|
+
function sourceFilterSql(source) {
|
|
16119
|
+
switch (source) {
|
|
16120
|
+
case "manual": return sql`(${KNOWN_NON_MANUAL_PREDICATE}) IS NOT TRUE`;
|
|
16121
|
+
case "github_issue": return sql`(c.metadata->>'source' = 'github' AND c.metadata->>'entityType' = 'issue')`;
|
|
16122
|
+
case "github_pull_request": return sql`(c.metadata->>'source' = 'github' AND c.metadata->>'entityType' = 'pull_request')`;
|
|
16123
|
+
case "github_discussion": return sql`(c.metadata->>'source' = 'github' AND c.metadata->>'entityType' = 'discussion')`;
|
|
16124
|
+
case "github_commit": return sql`(c.metadata->>'source' = 'github' AND c.metadata->>'entityType' = 'commit')`;
|
|
16125
|
+
case "feishu": return sql`(c.metadata->>'source' = 'feishu')`;
|
|
16126
|
+
}
|
|
15844
16127
|
}
|
|
15845
16128
|
/**
|
|
15846
16129
|
* GET /me/chats — cursor-paginated conversation list.
|
|
@@ -15866,6 +16149,7 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
|
15866
16149
|
const filterUnreadOnly = query.filter === "unread";
|
|
15867
16150
|
const filterWatchingOnly = query.filter === "watching";
|
|
15868
16151
|
const engagementPredicate = ENGAGEMENT_VIEW_PREDICATE[query.engagement];
|
|
16152
|
+
const sourcePredicate = query.source ? sourceFilterSql(query.source) : sql`TRUE`;
|
|
15869
16153
|
const cursorTsIso = cursor?.lastMessageAt ? cursor.lastMessageAt.toISOString() : null;
|
|
15870
16154
|
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
16155
|
OR c.last_message_at < ${cursorTsIso}::timestamptz
|
|
@@ -15882,7 +16166,7 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
|
15882
16166
|
WHERE chat_id = c.id AND access_mode = 'speaker') AS participant_count,
|
|
15883
16167
|
cm.access_mode AS access_mode,
|
|
15884
16168
|
COALESCE(cus.unread_mention_count, 0) AS unread_mention_count,
|
|
15885
|
-
COALESCE(cus.engagement_status, ${ACTIVE}) AS engagement_status
|
|
16169
|
+
COALESCE(cus.engagement_status, ${ACTIVE$1}) AS engagement_status
|
|
15886
16170
|
FROM chats c
|
|
15887
16171
|
JOIN chat_membership cm
|
|
15888
16172
|
ON cm.chat_id = c.id AND cm.agent_id = ${humanAgentId}
|
|
@@ -15898,6 +16182,7 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
|
15898
16182
|
AND (${!filterUnreadOnly}::bool OR COALESCE(cus.unread_mention_count, 0) > 0)
|
|
15899
16183
|
AND (${!filterWatchingOnly}::bool OR cm.access_mode = 'watcher')
|
|
15900
16184
|
AND ${engagementPredicate}
|
|
16185
|
+
AND ${sourcePredicate}
|
|
15901
16186
|
AND ${cursorPredicate}
|
|
15902
16187
|
ORDER BY c.last_message_at DESC NULLS LAST, c.id DESC
|
|
15903
16188
|
LIMIT ${limit + 1}
|
|
@@ -15920,6 +16205,8 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
|
15920
16205
|
agentId: chatMembership.agentId,
|
|
15921
16206
|
displayName: agents.displayName,
|
|
15922
16207
|
type: agents.type,
|
|
16208
|
+
avatarColorToken: agents.avatarColorToken,
|
|
16209
|
+
avatarImageUpdatedAt: agents.avatarImageUpdatedAt,
|
|
15923
16210
|
sessionState: agentChatSessions.state
|
|
15924
16211
|
}).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
16212
|
const participantsByChat = /* @__PURE__ */ new Map();
|
|
@@ -15929,7 +16216,9 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
|
15929
16216
|
list.push({
|
|
15930
16217
|
agentId: p.agentId,
|
|
15931
16218
|
displayName: p.displayName,
|
|
15932
|
-
type: p.type
|
|
16219
|
+
type: p.type,
|
|
16220
|
+
avatarColorToken: p.avatarColorToken,
|
|
16221
|
+
avatarImageUrl: agentAvatarImageUrl(p.agentId, p.avatarImageUpdatedAt)
|
|
15933
16222
|
});
|
|
15934
16223
|
participantsByChat.set(p.chatId, list);
|
|
15935
16224
|
if (p.sessionState === "active") {
|
|
@@ -16177,6 +16466,66 @@ async function leaveMeChat(db, chatId, humanAgentId) {
|
|
|
16177
16466
|
return result;
|
|
16178
16467
|
}
|
|
16179
16468
|
/**
|
|
16469
|
+
* Used by future bell-badge / list-pill counts. The partial index
|
|
16470
|
+
* `idx_user_state_unread WHERE unread_mention_count > 0` bounds the
|
|
16471
|
+
* driving scan; we then join `chat_membership` + `chats` so the badge
|
|
16472
|
+
* stays consistent with `listMeChats`.
|
|
16473
|
+
*
|
|
16474
|
+
* Why the joins (not just a single-table count): per §11.4 a user's
|
|
16475
|
+
* `chat_user_state` row is **preserved on detach** so read state
|
|
16476
|
+
* survives a leave/rejoin cycle. Without the membership join, any
|
|
16477
|
+
* preserved row with `unread_mention_count > 0` would keep
|
|
16478
|
+
* contributing to the badge even though the chat no longer appears in
|
|
16479
|
+
* the list. The `chats` join applies the same org-scoping +
|
|
16480
|
+
* `parent_chat_id IS NULL` filter as `listMeChats` so the two counts
|
|
16481
|
+
* cannot drift in the cross-org pollution or nested-chat cases either.
|
|
16482
|
+
*
|
|
16483
|
+
* Engagement parity: deleted chats are excluded from `listMeChats`
|
|
16484
|
+
* (any `engagement` view), so the badge must exclude them too — otherwise
|
|
16485
|
+
* the user sees an unread red dot for a chat they've removed from view.
|
|
16486
|
+
*/
|
|
16487
|
+
/**
|
|
16488
|
+
* Per-source aggregate for the conversation-list tag bar.
|
|
16489
|
+
*
|
|
16490
|
+
* Returns one row per source the caller has at least one chat for, plus an
|
|
16491
|
+
* always-present `manual` entry (zero counts when there are no manual chats —
|
|
16492
|
+
* the workspace UI uses `manual` as its default tab and must render it even
|
|
16493
|
+
* when empty).
|
|
16494
|
+
*
|
|
16495
|
+
* Filtering matches `listMeChats` for the corresponding tab so the badges
|
|
16496
|
+
* cannot drift from the list: same membership join, same `parent_chat_id IS
|
|
16497
|
+
* NULL` and `organization_id` scopes, same engagement view, same
|
|
16498
|
+
* `chat_user_state.unread_mention_count` source.
|
|
16499
|
+
*/
|
|
16500
|
+
async function listMeChatSourceCounts(db, humanAgentId, organizationId, query) {
|
|
16501
|
+
const engagementPredicate = ENGAGEMENT_VIEW_PREDICATE[query.engagement];
|
|
16502
|
+
const rows = await db.execute(sql`
|
|
16503
|
+
SELECT
|
|
16504
|
+
${chatSourceSqlExpression} AS source,
|
|
16505
|
+
count(*)::int AS chat_count,
|
|
16506
|
+
count(*) FILTER (WHERE COALESCE(cus.unread_mention_count, 0) > 0)::int AS unread_chat_count
|
|
16507
|
+
FROM chats c
|
|
16508
|
+
JOIN chat_membership cm
|
|
16509
|
+
ON cm.chat_id = c.id AND cm.agent_id = ${humanAgentId}
|
|
16510
|
+
LEFT JOIN chat_user_state cus
|
|
16511
|
+
ON cus.chat_id = c.id AND cus.agent_id = ${humanAgentId}
|
|
16512
|
+
WHERE c.parent_chat_id IS NULL
|
|
16513
|
+
AND c.organization_id = ${organizationId}
|
|
16514
|
+
AND ${engagementPredicate}
|
|
16515
|
+
GROUP BY 1
|
|
16516
|
+
`);
|
|
16517
|
+
const counts = {};
|
|
16518
|
+
for (const row of rows) counts[row.source] = {
|
|
16519
|
+
chatCount: Number(row.chat_count),
|
|
16520
|
+
unreadChatCount: Number(row.unread_chat_count)
|
|
16521
|
+
};
|
|
16522
|
+
if (!counts.manual) counts.manual = {
|
|
16523
|
+
chatCount: 0,
|
|
16524
|
+
unreadChatCount: 0
|
|
16525
|
+
};
|
|
16526
|
+
return { counts };
|
|
16527
|
+
}
|
|
16528
|
+
/**
|
|
16180
16529
|
* Class C — resource-scoped chat routes. Mounted at
|
|
16181
16530
|
* `/api/v1/chats/:chatId/...`. The chat's `organizationId` locates its
|
|
16182
16531
|
* org; `requireChatAccess` resolves the caller's membership in that org
|
|
@@ -16197,7 +16546,9 @@ async function chatRoutes(app) {
|
|
|
16197
16546
|
const agentRows = participantAgentIds.length > 0 ? await app.db.select({
|
|
16198
16547
|
agentId: agents.uuid,
|
|
16199
16548
|
displayName: agents.displayName,
|
|
16200
|
-
type: agents.type
|
|
16549
|
+
type: agents.type,
|
|
16550
|
+
avatarColorToken: agents.avatarColorToken,
|
|
16551
|
+
avatarImageUpdatedAt: agents.avatarImageUpdatedAt
|
|
16201
16552
|
}).from(agents).where(inArray(agents.uuid, participantAgentIds)) : [];
|
|
16202
16553
|
const agentMeta = new Map(agentRows.map((a) => [a.agentId, a]));
|
|
16203
16554
|
const participantsForTitle = participants.map((p) => {
|
|
@@ -16205,7 +16556,9 @@ async function chatRoutes(app) {
|
|
|
16205
16556
|
return {
|
|
16206
16557
|
agentId: p.agentId,
|
|
16207
16558
|
displayName: meta?.displayName ?? p.agentId,
|
|
16208
|
-
type: meta?.type ?? "unknown"
|
|
16559
|
+
type: meta?.type ?? "unknown",
|
|
16560
|
+
avatarColorToken: meta?.avatarColorToken ?? null,
|
|
16561
|
+
avatarImageUrl: agentAvatarImageUrl(p.agentId, meta?.avatarImageUpdatedAt ?? null)
|
|
16209
16562
|
};
|
|
16210
16563
|
});
|
|
16211
16564
|
const title = resolveChatTitle(chat.topic, firstMessagePreview, participantsForTitle, scope.humanAgentId);
|
|
@@ -16684,6 +17037,9 @@ const WINDOW_DAYS = {
|
|
|
16684
17037
|
"7d": 7,
|
|
16685
17038
|
"30d": 30
|
|
16686
17039
|
};
|
|
17040
|
+
function contextTreeSnapshotWindowDays(window) {
|
|
17041
|
+
return WINDOW_DAYS[window];
|
|
17042
|
+
}
|
|
16687
17043
|
const snapshotCache = /* @__PURE__ */ new Map();
|
|
16688
17044
|
const remoteSyncPromises = /* @__PURE__ */ new Map();
|
|
16689
17045
|
const remoteLastSyncedAt = /* @__PURE__ */ new Map();
|
|
@@ -16727,6 +17083,7 @@ async function getContextTreeSnapshot(binding, window = CONTEXT_TREE_SNAPSHOT_WI
|
|
|
16727
17083
|
snapshotStatus: statusWarning?.stale ? "stale" : "active",
|
|
16728
17084
|
contextStatus: contextStatus(statusWarning),
|
|
16729
17085
|
summary,
|
|
17086
|
+
usage: emptyUsageSummary(window),
|
|
16730
17087
|
updates,
|
|
16731
17088
|
nodes: nodesWithGhosts,
|
|
16732
17089
|
edges: tree.edges,
|
|
@@ -16981,6 +17338,13 @@ function withSnapshotStatus(snapshot, syncedAt, warning) {
|
|
|
16981
17338
|
contextStatus: warning ? contextStatus(warning) : snapshot.contextStatus
|
|
16982
17339
|
};
|
|
16983
17340
|
}
|
|
17341
|
+
function emptyUsageSummary(window) {
|
|
17342
|
+
return {
|
|
17343
|
+
windowDays: WINDOW_DAYS[window],
|
|
17344
|
+
agentCount: 0,
|
|
17345
|
+
usageCount: 0
|
|
17346
|
+
};
|
|
17347
|
+
}
|
|
16984
17348
|
function isSafeBranchName(branch) {
|
|
16985
17349
|
if (branch.startsWith("-")) return false;
|
|
16986
17350
|
if (branch.includes("..") || branch.includes("@{") || branch.includes("\\")) return false;
|
|
@@ -17011,6 +17375,7 @@ function unavailableSnapshot(repo, branch, detail) {
|
|
|
17011
17375
|
removedCount: 0,
|
|
17012
17376
|
changedNodeCount: 0
|
|
17013
17377
|
},
|
|
17378
|
+
usage: emptyUsageSummary(CONTEXT_TREE_SNAPSHOT_WINDOWS.SEVEN_DAYS),
|
|
17014
17379
|
updates: [],
|
|
17015
17380
|
nodes: [],
|
|
17016
17381
|
edges: [],
|
|
@@ -17586,11 +17951,16 @@ async function contextTreeSnapshotRoutes(app) {
|
|
|
17586
17951
|
const orgId = await resolveUserPrimaryOrgId(app.db, userId);
|
|
17587
17952
|
const binding = orgId ? await getOrgContextTree(app.db, orgId) : {};
|
|
17588
17953
|
const githubToken = contextTreeGithubTokenForRepo(binding.repo, app.config.contextTreeSync);
|
|
17954
|
+
const window = query.window ?? "7d";
|
|
17589
17955
|
const snapshot = await getContextTreeSnapshot({
|
|
17590
17956
|
...binding,
|
|
17591
17957
|
githubToken
|
|
17592
|
-
},
|
|
17593
|
-
|
|
17958
|
+
}, window);
|
|
17959
|
+
const usage = orgId ? await summarizeContextTreeUsage(app.db, orgId, contextTreeSnapshotWindowDays(window)) : snapshot.usage;
|
|
17960
|
+
return contextTreeSnapshotSchema.parse({
|
|
17961
|
+
...snapshot,
|
|
17962
|
+
usage
|
|
17963
|
+
});
|
|
17594
17964
|
});
|
|
17595
17965
|
}
|
|
17596
17966
|
function contextTreeGithubTokenForRepo(repo, syncConfig) {
|
|
@@ -17719,7 +18089,7 @@ async function healthzRoutes(app) {
|
|
|
17719
18089
|
* `api/orgs/invitations.ts` (Class B, admin-gated).
|
|
17720
18090
|
*/
|
|
17721
18091
|
async function publicInvitationRoutes(app) {
|
|
17722
|
-
const { previewInvitation } = await import("./invitation-CNv7gfFF-
|
|
18092
|
+
const { previewInvitation } = await import("./invitation-CNv7gfFF-DOFZ75wb.mjs");
|
|
17723
18093
|
app.get("/:token/preview", async (request, reply) => {
|
|
17724
18094
|
if (!request.params.token) throw new UnauthorizedError("Token required");
|
|
17725
18095
|
const preview = await previewInvitation(app.db, request.params.token);
|
|
@@ -17798,6 +18168,7 @@ async function meRoutes(app) {
|
|
|
17798
18168
|
createdAt: m.createdAt
|
|
17799
18169
|
})));
|
|
17800
18170
|
const defaultOrgId = defaultMembership ? memberships.find((m) => m.memberId === defaultMembership.id)?.organizationId ?? null : null;
|
|
18171
|
+
const memberCounts = await countActiveMembersByOrgs(app.db, memberships.map((mb) => mb.organizationId));
|
|
17801
18172
|
let inviteUrl = null;
|
|
17802
18173
|
if (defaultOrgId) {
|
|
17803
18174
|
if (memberships.find((m) => m.organizationId === defaultOrgId)?.role === "admin") {
|
|
@@ -17814,7 +18185,8 @@ async function meRoutes(app) {
|
|
|
17814
18185
|
organizationId: mb.organizationId,
|
|
17815
18186
|
organizationName: mb.orgDisplayName,
|
|
17816
18187
|
role: mb.role,
|
|
17817
|
-
agentId: mb.agentId
|
|
18188
|
+
agentId: mb.agentId,
|
|
18189
|
+
orgHasOtherMembers: (memberCounts.get(mb.organizationId) ?? 1) > 1
|
|
17818
18190
|
})),
|
|
17819
18191
|
onboarding: {
|
|
17820
18192
|
step: onboardingStep,
|
|
@@ -18006,7 +18378,7 @@ async function meRoutes(app) {
|
|
|
18006
18378
|
*/
|
|
18007
18379
|
app.get("/me/pinned-agents", async (request) => {
|
|
18008
18380
|
const { userId } = requireUser(request);
|
|
18009
|
-
const { listMyPinnedAgents } = await import("./client-
|
|
18381
|
+
const { listMyPinnedAgents } = await import("./client-gSnsRu5W-v_mC1sRY.mjs");
|
|
18010
18382
|
return listMyPinnedAgents(app.db, { userId });
|
|
18011
18383
|
});
|
|
18012
18384
|
/**
|
|
@@ -18136,6 +18508,80 @@ async function inferOnboardingStep(app, userId) {
|
|
|
18136
18508
|
if (!hasAgent) return "create_agent";
|
|
18137
18509
|
return "completed";
|
|
18138
18510
|
}
|
|
18511
|
+
const MAX_DOC_BYTES = 5 * 1024 * 1024;
|
|
18512
|
+
async function getMeDocPreview(input) {
|
|
18513
|
+
const workspaceRootReal = await realpathOrNotFound(join(input.workspacesRoot ?? join(DEFAULT_DATA_DIR$1, "workspaces"), input.agentName, input.chatId));
|
|
18514
|
+
const candidate = resolve(workspaceRootReal, join(input.basePath ?? "", input.path));
|
|
18515
|
+
if (extname(assertInsideWorkspace(workspaceRootReal, candidate)).toLowerCase() !== ".md") throw new ForbiddenError("Document preview only supports markdown files in the agent workspace");
|
|
18516
|
+
let fileStat;
|
|
18517
|
+
try {
|
|
18518
|
+
fileStat = await stat(candidate);
|
|
18519
|
+
} catch {
|
|
18520
|
+
throw new NotFoundError("Document not found");
|
|
18521
|
+
}
|
|
18522
|
+
if (!fileStat.isFile()) throw new NotFoundError("Document not found");
|
|
18523
|
+
if (fileStat.size > MAX_DOC_BYTES) throw new AppError(413, "Document is larger than the 5MB preview limit");
|
|
18524
|
+
const fileReal = await realpath(candidate);
|
|
18525
|
+
const normalizedPath = assertInsideWorkspace(workspaceRootReal, fileReal);
|
|
18526
|
+
const refPath = normalizeRefPath(input.path);
|
|
18527
|
+
const normalizedBasePath = input.basePath ? normalizeRefPath(input.basePath) : void 0;
|
|
18528
|
+
return {
|
|
18529
|
+
ref: {
|
|
18530
|
+
type: "workspace",
|
|
18531
|
+
chatId: input.chatId,
|
|
18532
|
+
agentId: input.agentId,
|
|
18533
|
+
...normalizedBasePath ? { basePath: normalizedBasePath } : {},
|
|
18534
|
+
path: refPath
|
|
18535
|
+
},
|
|
18536
|
+
path: normalizedPath,
|
|
18537
|
+
content: await readFile(fileReal, "utf8")
|
|
18538
|
+
};
|
|
18539
|
+
}
|
|
18540
|
+
async function realpathOrNotFound(path) {
|
|
18541
|
+
try {
|
|
18542
|
+
return await realpath(path);
|
|
18543
|
+
} catch {
|
|
18544
|
+
throw new NotFoundError("Document not found");
|
|
18545
|
+
}
|
|
18546
|
+
}
|
|
18547
|
+
function assertInsideWorkspace(workspaceRoot, target) {
|
|
18548
|
+
const rel = relative(workspaceRoot, target);
|
|
18549
|
+
if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new ForbiddenError("Document path must stay inside the agent workspace");
|
|
18550
|
+
return rel.split(sep).join("/");
|
|
18551
|
+
}
|
|
18552
|
+
function normalizeRefPath(path) {
|
|
18553
|
+
const parts = [];
|
|
18554
|
+
for (const part of path.split(/[\\/]/)) {
|
|
18555
|
+
if (!part || part === ".") continue;
|
|
18556
|
+
if (part === "..") {
|
|
18557
|
+
if (parts.length === 0) throw new ForbiddenError("Document path must stay inside the agent workspace");
|
|
18558
|
+
parts.pop();
|
|
18559
|
+
continue;
|
|
18560
|
+
}
|
|
18561
|
+
parts.push(part);
|
|
18562
|
+
}
|
|
18563
|
+
if (parts.length === 0) throw new ForbiddenError("Document path must name a markdown file");
|
|
18564
|
+
return parts.join("/");
|
|
18565
|
+
}
|
|
18566
|
+
async function meDocsRoutes(app, options = {}) {
|
|
18567
|
+
app.get("/chats/:chatId/docs/preview", async (request) => {
|
|
18568
|
+
await requireChatAccess(request, app.db);
|
|
18569
|
+
const query = getMeDocSchema.parse(request.query);
|
|
18570
|
+
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);
|
|
18571
|
+
if (!participant) throw new NotFoundError("Document not found");
|
|
18572
|
+
const [agent] = await app.db.select({ name: agents.name }).from(agents).where(eq(agents.uuid, query.agentId)).limit(1);
|
|
18573
|
+
if (!agent?.name) throw new NotFoundError("Document not found");
|
|
18574
|
+
const preview = await getMeDocPreview({
|
|
18575
|
+
chatId: request.params.chatId,
|
|
18576
|
+
agentId: query.agentId,
|
|
18577
|
+
agentName: agent.name,
|
|
18578
|
+
basePath: query.basePath,
|
|
18579
|
+
path: query.path,
|
|
18580
|
+
workspacesRoot: options.workspacesRoot
|
|
18581
|
+
});
|
|
18582
|
+
return getMeDocResponseSchema.parse(preview);
|
|
18583
|
+
});
|
|
18584
|
+
}
|
|
18139
18585
|
/**
|
|
18140
18586
|
* Resolve the caller's active membership in `:orgId` (from the URL) and
|
|
18141
18587
|
* return the full `OrgScope`. The type signature requires
|
|
@@ -18316,7 +18762,7 @@ async function orgAgentRoutes(app) {
|
|
|
18316
18762
|
const { type } = listAgentsFilterSchema.parse(request.query);
|
|
18317
18763
|
const result = await listAgentsForMember(app.db, scope, query.limit, query.cursor, type);
|
|
18318
18764
|
return {
|
|
18319
|
-
items: result.items.map((a) => ({
|
|
18765
|
+
items: result.items.map(({ avatarImageUpdatedAt, ...a }) => ({
|
|
18320
18766
|
...a,
|
|
18321
18767
|
managerId: a.managerId ?? null,
|
|
18322
18768
|
presenceStatus: a.presenceStatus ?? "offline",
|
|
@@ -18325,7 +18771,8 @@ async function orgAgentRoutes(app) {
|
|
|
18325
18771
|
clientId: a.clientId ?? null,
|
|
18326
18772
|
runtimeType: a.runtimeType ?? null,
|
|
18327
18773
|
runtimeState: a.runtimeState ?? null,
|
|
18328
|
-
activeSessions: a.activeSessions ?? null
|
|
18774
|
+
activeSessions: a.activeSessions ?? null,
|
|
18775
|
+
avatarImageUrl: agentAvatarImageUrl(a.uuid, avatarImageUpdatedAt)
|
|
18329
18776
|
})),
|
|
18330
18777
|
nextCursor: result.nextCursor
|
|
18331
18778
|
};
|
|
@@ -18341,7 +18788,7 @@ async function orgAgentRoutes(app) {
|
|
|
18341
18788
|
const query = paginationQuerySchema.parse(request.query);
|
|
18342
18789
|
const result = await listAgentsForAdmin(app.db, scope, query.limit, query.cursor);
|
|
18343
18790
|
return {
|
|
18344
|
-
items: result.items.map((a) => ({
|
|
18791
|
+
items: result.items.map(({ avatarImageUpdatedAt, ...a }) => ({
|
|
18345
18792
|
...a,
|
|
18346
18793
|
managerId: a.managerId ?? null,
|
|
18347
18794
|
presenceStatus: a.presenceStatus ?? "offline",
|
|
@@ -18350,7 +18797,8 @@ async function orgAgentRoutes(app) {
|
|
|
18350
18797
|
clientId: a.clientId ?? null,
|
|
18351
18798
|
runtimeType: a.runtimeType ?? null,
|
|
18352
18799
|
runtimeState: a.runtimeState ?? null,
|
|
18353
|
-
activeSessions: a.activeSessions ?? null
|
|
18800
|
+
activeSessions: a.activeSessions ?? null,
|
|
18801
|
+
avatarImageUrl: agentAvatarImageUrl(a.uuid, avatarImageUpdatedAt)
|
|
18354
18802
|
})),
|
|
18355
18803
|
nextCursor: result.nextCursor
|
|
18356
18804
|
};
|
|
@@ -18443,6 +18891,17 @@ async function orgChatRoutes(app) {
|
|
|
18443
18891
|
return listMeChats(app.db, scope.humanAgentId, scope.organizationId, query);
|
|
18444
18892
|
});
|
|
18445
18893
|
/**
|
|
18894
|
+
* GET /orgs/:orgId/chats/source-counts — per-source aggregate powering the
|
|
18895
|
+
* conversation-list tag bar (Manual / GitHub PR / GitHub Issue / Feishu).
|
|
18896
|
+
* Returns counts only for sources the caller has chats in, plus an
|
|
18897
|
+
* always-present `manual` entry. Same engagement view filter as the list.
|
|
18898
|
+
*/
|
|
18899
|
+
app.get("/source-counts", async (request) => {
|
|
18900
|
+
const scope = await requireOrgMembership(request, app.db);
|
|
18901
|
+
const query = listMeChatSourceCountsQuerySchema.parse(request.query);
|
|
18902
|
+
return listMeChatSourceCounts(app.db, scope.humanAgentId, scope.organizationId, query);
|
|
18903
|
+
});
|
|
18904
|
+
/**
|
|
18446
18905
|
* POST /orgs/:orgId/chats — create a new chat. The :orgId path param
|
|
18447
18906
|
* makes the org explicit; visibility of every requested participant is
|
|
18448
18907
|
* verified before the service layer touches the DB.
|
|
@@ -18496,11 +18955,16 @@ async function orgContextTreeSnapshotRoutes(app) {
|
|
|
18496
18955
|
const scope = await requireOrgMembership(request, app.db);
|
|
18497
18956
|
const binding = await getOrgContextTree(app.db, scope.organizationId);
|
|
18498
18957
|
const githubToken = contextTreeGithubTokenForRepo(binding.repo, app.config.contextTreeSync);
|
|
18958
|
+
const window = query.window ?? "7d";
|
|
18499
18959
|
const snapshot = await getContextTreeSnapshot({
|
|
18500
18960
|
...binding,
|
|
18501
18961
|
githubToken
|
|
18502
|
-
},
|
|
18503
|
-
|
|
18962
|
+
}, window);
|
|
18963
|
+
const usage = await summarizeContextTreeUsage(app.db, scope.organizationId, contextTreeSnapshotWindowDays(window));
|
|
18964
|
+
return contextTreeSnapshotSchema.parse({
|
|
18965
|
+
...snapshot,
|
|
18966
|
+
usage
|
|
18967
|
+
});
|
|
18504
18968
|
});
|
|
18505
18969
|
}
|
|
18506
18970
|
function orgIdParam(params) {
|
|
@@ -19141,6 +19605,64 @@ const githubEntityChatMappings = pgTable("github_entity_chat_mappings", {
|
|
|
19141
19605
|
table.entityType,
|
|
19142
19606
|
table.entityKey
|
|
19143
19607
|
] }), index("idx_github_entity_chat_mappings_chat").on(table.chatId)]);
|
|
19608
|
+
/**
|
|
19609
|
+
* Auto-archive chats when one of their bound pull requests is merged.
|
|
19610
|
+
*
|
|
19611
|
+
* Trigger: GitHub `pull_request.closed` webhook with `merged === true`. The
|
|
19612
|
+
* webhook handler calls this service on a bypass branch — the normalize /
|
|
19613
|
+
* audience / deliver pipeline is unaffected (PR closed events still drop in
|
|
19614
|
+
* Stage 1).
|
|
19615
|
+
*
|
|
19616
|
+
* Algorithm: a merged PR flips every chat bound to it into the user's
|
|
19617
|
+
* archived view, with no inspection of sibling PR state. Multi-PR chats can
|
|
19618
|
+
* be temporarily archived while siblings are still open; any later activity
|
|
19619
|
+
* on those siblings produces a normal delivery message, which the existing
|
|
19620
|
+
* chat-projection auto-revives back to `active`. This trades perfect timing
|
|
19621
|
+
* for zero local state, no GitHub API calls, and no schema changes.
|
|
19622
|
+
*
|
|
19623
|
+
* Per-user safety: writes use an UPSERT guarded with
|
|
19624
|
+
* `setWhere = engagement_status = 'active'`, so only the implicit-active or
|
|
19625
|
+
* explicitly-active rows flip. User-manually `deleted` and already-`archived`
|
|
19626
|
+
* rows are left alone. Idempotent under GitHub webhook retries.
|
|
19627
|
+
*/
|
|
19628
|
+
const { ACTIVE, ARCHIVED } = CHAT_ENGAGEMENT_STATUSES;
|
|
19629
|
+
async function archiveChatsForMergedPr(db, input) {
|
|
19630
|
+
if (!input.repoFullName || !Number.isFinite(input.prNumber) || input.prNumber <= 0) return {
|
|
19631
|
+
chats: 0,
|
|
19632
|
+
rowsConsidered: 0
|
|
19633
|
+
};
|
|
19634
|
+
const entityKey = `${input.repoFullName}#${input.prNumber}`;
|
|
19635
|
+
const rows = await db.select({
|
|
19636
|
+
chatId: githubEntityChatMappings.chatId,
|
|
19637
|
+
humanAgentId: githubEntityChatMappings.humanAgentId
|
|
19638
|
+
}).from(githubEntityChatMappings).where(and(eq(githubEntityChatMappings.organizationId, input.organizationId), eq(githubEntityChatMappings.entityType, "pull_request"), eq(githubEntityChatMappings.entityKey, entityKey)));
|
|
19639
|
+
if (rows.length === 0) return {
|
|
19640
|
+
chats: 0,
|
|
19641
|
+
rowsConsidered: 0
|
|
19642
|
+
};
|
|
19643
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19644
|
+
const targets = [];
|
|
19645
|
+
for (const row of rows) {
|
|
19646
|
+
const key = `${row.chatId}|${row.humanAgentId}`;
|
|
19647
|
+
if (seen.has(key)) continue;
|
|
19648
|
+
seen.add(key);
|
|
19649
|
+
targets.push(row);
|
|
19650
|
+
}
|
|
19651
|
+
for (const { chatId, humanAgentId } of targets) await db.insert(chatUserState).values({
|
|
19652
|
+
chatId,
|
|
19653
|
+
agentId: humanAgentId,
|
|
19654
|
+
unreadMentionCount: 0,
|
|
19655
|
+
engagementStatus: ARCHIVED
|
|
19656
|
+
}).onConflictDoUpdate({
|
|
19657
|
+
target: [chatUserState.chatId, chatUserState.agentId],
|
|
19658
|
+
set: { engagementStatus: ARCHIVED },
|
|
19659
|
+
setWhere: eq(chatUserState.engagementStatus, ACTIVE)
|
|
19660
|
+
});
|
|
19661
|
+
return {
|
|
19662
|
+
chats: new Set(targets.map((t) => t.chatId)).size,
|
|
19663
|
+
rowsConsidered: targets.length
|
|
19664
|
+
};
|
|
19665
|
+
}
|
|
19144
19666
|
function evaluateDelegateTarget(target, sourceOrgId) {
|
|
19145
19667
|
if (!target) return "not_found";
|
|
19146
19668
|
if (target.organizationId !== sourceOrgId) return "cross_org";
|
|
@@ -20286,6 +20808,28 @@ async function githubAppWebhookRoutes(app) {
|
|
|
20286
20808
|
};
|
|
20287
20809
|
const deliveryHeader = request.headers["x-github-delivery"];
|
|
20288
20810
|
const deliveryId = typeof deliveryHeader === "string" && deliveryHeader.length > 0 ? deliveryHeader : null;
|
|
20811
|
+
if (eventType === "pull_request" && isRecord(payload) && payload.action === "closed") {
|
|
20812
|
+
const pr = isRecord(payload.pull_request) ? payload.pull_request : null;
|
|
20813
|
+
const repoFullName = readString$1((isRecord(payload.repository) ? payload.repository : null)?.full_name);
|
|
20814
|
+
const prNumber = readNumber$1(pr?.number);
|
|
20815
|
+
if (pr?.merged === true && repoFullName && prNumber !== null) try {
|
|
20816
|
+
const stats = await archiveChatsForMergedPr(app.db, {
|
|
20817
|
+
organizationId: installation.hubOrganizationId,
|
|
20818
|
+
repoFullName,
|
|
20819
|
+
prNumber
|
|
20820
|
+
});
|
|
20821
|
+
log$1.info({
|
|
20822
|
+
entityKey: `${repoFullName}#${prNumber}`,
|
|
20823
|
+
...stats
|
|
20824
|
+
}, "auto-archived chats on PR merge");
|
|
20825
|
+
} catch (err) {
|
|
20826
|
+
log$1.error({
|
|
20827
|
+
err,
|
|
20828
|
+
repoFullName,
|
|
20829
|
+
prNumber
|
|
20830
|
+
}, "failed to auto-archive chats on PR merge");
|
|
20831
|
+
}
|
|
20832
|
+
}
|
|
20289
20833
|
const event = normalizeGithubEvent(eventType, payload, source, deliveryId);
|
|
20290
20834
|
if (!event) {
|
|
20291
20835
|
log$1.debug({
|
|
@@ -21886,12 +22430,14 @@ async function buildApp(config) {
|
|
|
21886
22430
|
await api.register(githubOauthRoutes, { prefix: "/auth/github" });
|
|
21887
22431
|
await api.register(publicInvitationRoutes, { prefix: "/invitations" });
|
|
21888
22432
|
await api.register(bootstrapConfigRoutes, { prefix: "/bootstrap" });
|
|
22433
|
+
await api.register(publicAgentAvatarRoutes, { prefix: "/agents" });
|
|
21889
22434
|
await api.register(userScope("contextTreeScope", async (scope) => {
|
|
21890
22435
|
await scope.register(contextTreeInfoRoutes);
|
|
21891
22436
|
await scope.register(contextTreeSnapshotRoutes);
|
|
21892
22437
|
}), { prefix: "/context-tree" });
|
|
21893
22438
|
await api.register(userScope("meRoutesScope", async (scope) => {
|
|
21894
22439
|
await scope.register(meRoutes);
|
|
22440
|
+
await scope.register(meDocsRoutes, { workspacesRoot: config.workspace.root });
|
|
21895
22441
|
}), { prefix: "" });
|
|
21896
22442
|
await api.register(userScope("orgsScope", async (scope) => {
|
|
21897
22443
|
await scope.register(orgIdentityRoutes);
|