@agent-team-foundation/first-tree-hub 0.12.10 → 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.
@@ -1,10 +1,10 @@
1
1
  import { O as withSpan, f as messageAttrs, s as createLogger } from "./observability-BAScT_5S-BcW9HgkG.mjs";
2
- import { F as extractMentions, b as clientCapabilitiesSchema, i as AGENT_STATUSES, it as questionMessageContentSchema, j as defaultParticipantMode, lt as scanMentionTokens, o as AGENT_VISIBILITY, rt as questionAnswerMessageContentSchema, s as CHAT_ENGAGEMENT_STATUSES, v as agentTypeSchema } from "./dist-B1GHzMLc.mjs";
3
- import { a as ConflictError, i as ClientUserMismatchError, l as organizations, n as BadRequestError, o as ForbiddenError, s as NotFoundError, u as users } from "./errors-CF5evtJt-B0NTIVPt.mjs";
2
+ import { L as extractMentions, N as defaultParticipantMode, S as clientCapabilitiesSchema, b as agentTypeSchema, ct as questionAnswerMessageContentSchema, i as AGENT_STATUSES, lt as questionMessageContentSchema, mt as scanMentionTokens, o as AGENT_VISIBILITY, s as CHAT_ENGAGEMENT_STATUSES } from "./dist-1XGLJMOq.mjs";
3
+ import { a as ConflictError, i as ClientUserMismatchError, l as organizations, n as BadRequestError, o as ForbiddenError, s as NotFoundError, u as users } from "./errors-LPcARA4K-Dbrptiyz.mjs";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { and, desc, eq, inArray, isNotNull, lt, ne, or, sql } from "drizzle-orm";
6
- import { bigserial, boolean, index, integer, jsonb, pgTable, primaryKey, text, timestamp, unique } from "drizzle-orm/pg-core";
7
- //#region ../server/dist/client-BViGcaUC.mjs
6
+ import { bigserial, boolean, customType, index, integer, jsonb, pgTable, primaryKey, text, timestamp, unique } from "drizzle-orm/pg-core";
7
+ //#region ../server/dist/client-RM_03B_l.mjs
8
8
  /**
9
9
  * Client connections. A client is a single SDK process (AgentRuntime) that may
10
10
  * host multiple agents. From the unified-user-token milestone on, a client is
@@ -33,6 +33,13 @@ const clients = pgTable("clients", {
33
33
  lastSeenAt: timestamp("last_seen_at", { withTimezone: true }).notNull().defaultNow(),
34
34
  metadata: jsonb("metadata").$type()
35
35
  }, (table) => [index("idx_clients_user").on(table.userId), index("idx_clients_org").on(table.organizationId)]);
36
+ /**
37
+ * `bytea` column type — Drizzle ships pg primitives but not bytea out of the
38
+ * box. Reads come back as Node `Buffer` (postgres-js); writes accept any
39
+ * `Uint8Array`. Used for the small inline avatar image blob; no streaming
40
+ * needed at this size (≤ ~50 KB after client-side resize).
41
+ */
42
+ const bytea = customType({ dataType: () => "bytea" });
36
43
  /** Agent registration. Each agent owns a unique inboxId for message delivery. */
37
44
  const agents = pgTable("agents", {
38
45
  uuid: text("uuid").primaryKey(),
@@ -49,6 +56,10 @@ const agents = pgTable("agents", {
49
56
  managerId: text("manager_id").notNull(),
50
57
  clientId: text("client_id").references(() => clients.id, { onDelete: "restrict" }),
51
58
  runtimeProvider: text("runtime_provider").notNull().default("claude-code"),
59
+ avatarColorToken: text("avatar_color_token"),
60
+ avatarImageData: bytea("avatar_image_data"),
61
+ avatarImageMime: text("avatar_image_mime"),
62
+ avatarImageUpdatedAt: timestamp("avatar_image_updated_at", { withTimezone: true }),
52
63
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
53
64
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
54
65
  }, (table) => [
@@ -1306,6 +1317,7 @@ const pendingQuestions = pgTable("pending_questions", {
1306
1317
  const INBOX_CHANNEL = "inbox_notifications";
1307
1318
  const CONFIG_CHANNEL = "config_changes";
1308
1319
  const SESSION_STATE_CHANNEL = "session_state_changes";
1320
+ const SESSION_EVENT_CHANNEL = "session_event_changes";
1309
1321
  const RUNTIME_STATE_CHANNEL = "runtime_state_changes";
1310
1322
  /**
1311
1323
  * Chat-first workspace cross-process kick. Carries `<chatId>:<messageId>`.
@@ -1326,12 +1338,14 @@ function createNotifier(listenClient) {
1326
1338
  const subscriptions = /* @__PURE__ */ new Map();
1327
1339
  const configChangeHandlers = [];
1328
1340
  const sessionStateChangeHandlers = [];
1341
+ const sessionEventHandlers = [];
1329
1342
  const runtimeStateChangeHandlers = [];
1330
1343
  const chatMessageHandlers = [];
1331
1344
  const adminBroadcastHandlers = [];
1332
1345
  let unlistenInboxFn = null;
1333
1346
  let unlistenConfigFn = null;
1334
1347
  let unlistenSessionStateFn = null;
1348
+ let unlistenSessionEventFn = null;
1335
1349
  let unlistenRuntimeStateFn = null;
1336
1350
  let unlistenChatMessageFn = null;
1337
1351
  let unlistenAdminBroadcastFn = null;
@@ -1384,6 +1398,11 @@ function createNotifier(listenClient) {
1384
1398
  await listenClient`SELECT pg_notify(${SESSION_STATE_CHANNEL}, ${`${agentId}:${chatId}:${state}:${organizationId}`})`;
1385
1399
  } catch {}
1386
1400
  },
1401
+ async notifySessionEvent(agentId, chatId, kind, organizationId) {
1402
+ try {
1403
+ await listenClient`SELECT pg_notify(${SESSION_EVENT_CHANNEL}, ${`${agentId}:${chatId}:${kind}:${organizationId}`})`;
1404
+ } catch {}
1405
+ },
1387
1406
  async notifyRuntimeStateChange(agentId, state, organizationId) {
1388
1407
  try {
1389
1408
  await listenClient`SELECT pg_notify(${RUNTIME_STATE_CHANNEL}, ${`${agentId}:${state}:${organizationId}`})`;
@@ -1432,6 +1451,9 @@ function createNotifier(listenClient) {
1432
1451
  onSessionStateChange(handler) {
1433
1452
  sessionStateChangeHandlers.push(handler);
1434
1453
  },
1454
+ onSessionEvent(handler) {
1455
+ sessionEventHandlers.push(handler);
1456
+ },
1435
1457
  onRuntimeStateChange(handler) {
1436
1458
  runtimeStateChangeHandlers.push(handler);
1437
1459
  },
@@ -1467,6 +1489,27 @@ function createNotifier(listenClient) {
1467
1489
  }
1468
1490
  }
1469
1491
  })).unlisten;
1492
+ unlistenSessionEventFn = (await listenClient.listen(SESSION_EVENT_CHANNEL, (payload) => {
1493
+ if (payload) {
1494
+ const firstSep = payload.indexOf(":");
1495
+ const secondSep = payload.indexOf(":", firstSep + 1);
1496
+ const thirdSep = payload.indexOf(":", secondSep + 1);
1497
+ if (firstSep > 0 && secondSep > firstSep && thirdSep > secondSep) {
1498
+ const agentId = payload.slice(0, firstSep);
1499
+ const chatId = payload.slice(firstSep + 1, secondSep);
1500
+ const kind = payload.slice(secondSep + 1, thirdSep);
1501
+ const organizationId = payload.slice(thirdSep + 1);
1502
+ for (const handler of sessionEventHandlers) try {
1503
+ handler({
1504
+ agentId,
1505
+ chatId,
1506
+ kind,
1507
+ organizationId
1508
+ });
1509
+ } catch {}
1510
+ }
1511
+ }
1512
+ })).unlisten;
1470
1513
  unlistenRuntimeStateFn = (await listenClient.listen(RUNTIME_STATE_CHANNEL, (payload) => {
1471
1514
  if (payload) {
1472
1515
  const firstSep = payload.indexOf(":");
@@ -1524,6 +1567,10 @@ function createNotifier(listenClient) {
1524
1567
  await unlistenSessionStateFn();
1525
1568
  unlistenSessionStateFn = null;
1526
1569
  }
1570
+ if (unlistenSessionEventFn) {
1571
+ await unlistenSessionEventFn();
1572
+ unlistenSessionEventFn = null;
1573
+ }
1527
1574
  if (unlistenRuntimeStateFn) {
1528
1575
  await unlistenRuntimeStateFn();
1529
1576
  unlistenRuntimeStateFn = null;
@@ -1748,7 +1795,7 @@ async function sendMessageInner(db, chatId, senderId, data, options) {
1748
1795
  } : incomingMeta;
1749
1796
  const dmAutoProjection = chatType === "direct" ? [...new Set([...mergedMentions, ...participants.filter((p) => p.agentId !== senderId).map((p) => p.agentId)])] : mergedMentions;
1750
1797
  if (options.enforceGroupMention && chatType === "group") {
1751
- if (mergedMentions.filter((id) => id !== senderId).length === 0) throw new BadRequestError("Sending to a group chat requires an explicit @mention. Use `agent send <name>` to message a single agent, or @<name> in the content to address one or more group members.");
1798
+ if (mergedMentions.filter((id) => id !== senderId).length === 0) throw new BadRequestError("Sending to a group chat requires an explicit @mention. Use `first-tree-hub chat send <name>` to message a single agent, or @<name> in the content to address one or more group members.");
1752
1799
  }
1753
1800
  let outboundContent = data.content;
1754
1801
  if (options.normalizeMentionsInContent && typeof outboundContent === "string") {
@@ -1933,7 +1980,7 @@ async function sendToAgent(db, senderUuid, targetName, data) {
1933
1980
  }).from(agents).where(eq(agents.uuid, senderUuid)).limit(1);
1934
1981
  if (!sender) throw new NotFoundError(`Agent "${senderUuid}" not found`);
1935
1982
  const [target] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, sender.organizationId), eq(agents.name, targetName), ne(agents.status, "deleted"))).limit(1);
1936
- if (!target) throw new NotFoundError(`Agent "${targetName}" not found${/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(targetName) ? " — `agent send` expects an agent NAME, not a uuid. Run `first-tree-hub agent list` to see available names." : ""}`);
1983
+ if (!target) throw new NotFoundError(`Agent "${targetName}" not found${/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(targetName) ? " — `first-tree-hub chat send` expects an agent NAME, not a uuid. Run `first-tree-hub agent list` to see available names." : ""}`);
1937
1984
  const incomingMeta = data.metadata ?? {};
1938
1985
  const existingMentionsRaw = incomingMeta.mentions;
1939
1986
  const existingMentions = Array.isArray(existingMentionsRaw) ? existingMentionsRaw.filter((m) => typeof m === "string") : [];
@@ -1,7 +1,7 @@
1
1
  import "./observability-BAScT_5S-BcW9HgkG.mjs";
2
2
  import "./logger-core-BTmvdflj-DjW8FM4T.mjs";
3
- import "./dist-B1GHzMLc.mjs";
4
- import "./errors-CF5evtJt-B0NTIVPt.mjs";
3
+ import "./dist-1XGLJMOq.mjs";
4
+ import "./errors-LPcARA4K-Dbrptiyz.mjs";
5
5
  import "./src-DNBS5Yjj.mjs";
6
- import { J as listMyPinnedAgents } from "./client-BViGcaUC-CZb2Svgh.mjs";
6
+ import { J as listMyPinnedAgents } from "./client-RM_03B_l-DiEIa9xe.mjs";
7
7
  export { listMyPinnedAgents };
@@ -185,6 +185,7 @@ z.object({
185
185
  const PROMPT_APPEND_MAX_LENGTH = 32e3;
186
186
  const MCP_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
187
187
  const ENV_KEY_PATTERN = /^[A-Z][A-Z0-9_]*$/;
188
+ const WINDOWS_DRIVE_PATH_PATTERN = /^[A-Za-z]:/;
188
189
  const promptConfigSchema = z.object({ append: z.string().max(PROMPT_APPEND_MAX_LENGTH).default("") });
189
190
  const mcpStdioServerSchema = z.object({
190
191
  name: z.string().regex(MCP_NAME_PATTERN, "MCP name must match /^[a-z0-9][a-z0-9_-]{0,63}$/i"),
@@ -214,10 +215,38 @@ const envEntrySchema = z.object({
214
215
  value: z.string(),
215
216
  sensitive: z.boolean().default(false)
216
217
  });
218
+ function hasControlCharacters(value) {
219
+ for (let idx = 0; idx < value.length; idx++) {
220
+ const code = value.charCodeAt(idx);
221
+ if (code <= 31 || code === 127) return true;
222
+ }
223
+ return false;
224
+ }
225
+ function getRepoLocalPathSafetyError(localPath) {
226
+ if (localPath.length === 0) return "Git repo local path must not be empty";
227
+ if (localPath.trim() !== localPath) return "Git repo local path must not have leading or trailing whitespace";
228
+ if (hasControlCharacters(localPath)) return "Git repo local path must not contain control characters";
229
+ if (localPath.includes("\\")) return "Git repo local path must use forward slashes";
230
+ if (localPath.startsWith("/") || WINDOWS_DRIVE_PATH_PATTERN.test(localPath)) return "Git repo local path must be relative";
231
+ const segments = localPath.split("/");
232
+ for (const segment of segments) {
233
+ if (!segment) return "Git repo local path must not contain empty path segments";
234
+ if (segment === "." || segment === "..") return "Git repo local path must not contain dot segments";
235
+ if (segment.trim() !== segment) return "Git repo local path segments must not have leading or trailing whitespace";
236
+ }
237
+ return null;
238
+ }
217
239
  const gitRepoSchema = z.object({
218
240
  url: z.string().min(1),
219
241
  ref: z.string().min(1).optional(),
220
- localPath: z.string().min(1).optional()
242
+ localPath: z.string().min(1).superRefine((localPath, ctx) => {
243
+ const safetyError = getRepoLocalPathSafetyError(localPath);
244
+ if (!safetyError) return;
245
+ ctx.addIssue({
246
+ code: z.ZodIssueCode.custom,
247
+ message: safetyError
248
+ });
249
+ }).optional()
221
250
  });
222
251
  /**
223
252
  * Untagged base shape — 5 user-tunable fields, no `kind` discriminator.
@@ -491,6 +520,16 @@ const AGENT_VISIBILITY = {
491
520
  ORGANIZATION: "organization"
492
521
  };
493
522
  const agentVisibilitySchema = z.enum(["private", "organization"]);
523
+ const avatarColorTokenSchema = z.enum([
524
+ "hue-0",
525
+ "hue-1",
526
+ "hue-2",
527
+ "hue-3",
528
+ "hue-4",
529
+ "hue-5",
530
+ "hue-6",
531
+ "hue-7"
532
+ ]);
494
533
  const AGENT_STATUSES = {
495
534
  ACTIVE: "active",
496
535
  SUSPENDED: "suspended",
@@ -542,7 +581,8 @@ const updateAgentSchema = z.object({
542
581
  visibility: agentVisibilitySchema.optional(),
543
582
  metadata: z.record(z.string(), z.unknown()).optional(),
544
583
  managerId: z.string().nullable().optional(),
545
- clientId: z.string().min(1).max(100).nullable().optional()
584
+ clientId: z.string().min(1).max(100).nullable().optional(),
585
+ avatarColorToken: avatarColorTokenSchema.nullable().optional()
546
586
  });
547
587
  /**
548
588
  * Service-level rebind input. Admin / owner re-binds an agent to a new
@@ -571,6 +611,8 @@ z.object({
571
611
  managerId: z.string().nullable(),
572
612
  clientId: z.string().nullable(),
573
613
  runtimeProvider: runtimeProviderSchema,
614
+ avatarColorToken: z.string().nullable(),
615
+ avatarImageUrl: z.string().nullable(),
574
616
  presenceStatus: presenceStatusSchema.optional(),
575
617
  createdAt: z.string(),
576
618
  updatedAt: z.string()
@@ -608,12 +650,30 @@ z.object({
608
650
  expiresIn: z.number(),
609
651
  command: z.string()
610
652
  });
611
- const githubEntityTypeSchema = z.enum([
653
+ /**
654
+ * `chats.metadata` is `jsonb` at the DB layer. Without a typed contract every
655
+ * writer was free to invent their own keys, so a GitHub webhook writing
656
+ * `{ source: "github", entityKey: "..." }` and a Feishu adapter writing
657
+ * `{ source: "feishu", externalChannelId: "..." }` could collide on shared
658
+ * keys (`source`, `title`). The discriminated union below pins both writers
659
+ * to a single shape and lets TypeScript narrow on `metadata.source`.
660
+ *
661
+ * Add new variants here; do not extend with free-form `Record<string, unknown>`.
662
+ */
663
+ /**
664
+ * Source of truth for which github entities get their own tag in the
665
+ * conversation list. Adding a new entry here extends `ChatSource`
666
+ * (`github_${entityType}`) and the SQL CASE/IN-list in
667
+ * `server/services/me-chat.ts` without touching them — both derive from
668
+ * this constant.
669
+ */
670
+ const GITHUB_ENTITY_TYPES = [
612
671
  "issue",
613
672
  "pull_request",
614
673
  "discussion",
615
674
  "commit"
616
- ]);
675
+ ];
676
+ const githubEntityTypeSchema = z.enum(GITHUB_ENTITY_TYPES);
617
677
  const githubChatMetadataSchema = z.object({
618
678
  source: z.literal("github"),
619
679
  entityType: githubEntityTypeSchema,
@@ -632,6 +692,14 @@ const chatMetadataSchema = z.discriminatedUnion("source", [githubChatMetadataSch
632
692
  * sneak through `{ source: "github" }` without the required fields.
633
693
  */
634
694
  const optionalChatMetadataSchema = z.union([z.object({}).strict(), chatMetadataSchema]);
695
+ const chatSourceSchema = z.enum([
696
+ "manual",
697
+ "github_issue",
698
+ "github_pull_request",
699
+ "github_discussion",
700
+ "github_commit",
701
+ "feishu"
702
+ ]);
635
703
  const chatTypeSchema = z.enum(["direct", "group"]);
636
704
  /**
637
705
  * Per-(chat, user) engagement state. Stored on `chat_user_state` so each
@@ -868,6 +936,11 @@ const contextTreeSummarySchema = z.object({
868
936
  removedCount: z.number().int().nonnegative(),
869
937
  changedNodeCount: z.number().int().nonnegative()
870
938
  });
939
+ const contextTreeUsageSummarySchema = z.object({
940
+ windowDays: z.number().int().positive(),
941
+ agentCount: z.number().int().nonnegative(),
942
+ usageCount: z.number().int().nonnegative()
943
+ });
871
944
  const contextTreeSnapshotSchema = z.object({
872
945
  repo: z.string().nullable(),
873
946
  branch: z.string().nullable(),
@@ -876,6 +949,7 @@ const contextTreeSnapshotSchema = z.object({
876
949
  snapshotStatus: contextTreeSnapshotStatusSchema,
877
950
  contextStatus: contextTreeStatusSchema,
878
951
  summary: contextTreeSummarySchema,
952
+ usage: contextTreeUsageSummarySchema,
879
953
  updates: z.array(contextTreeUpdateSchema),
880
954
  nodes: z.array(contextTreeNodeSchema),
881
955
  edges: z.array(contextTreeEdgeSchema),
@@ -1161,13 +1235,38 @@ const listMeChatsQuerySchema = z.object({
1161
1235
  cursor: z.string().optional(),
1162
1236
  limit: z.coerce.number().int().min(1).max(200).default(50),
1163
1237
  filter: meChatFilterSchema.default("all"),
1164
- engagement: chatEngagementViewSchema.default("active")
1238
+ engagement: chatEngagementViewSchema.default("active"),
1239
+ source: chatSourceSchema.optional()
1165
1240
  });
1166
1241
  const meChatParticipantSchema = z.object({
1167
1242
  agentId: z.string(),
1168
1243
  displayName: z.string(),
1169
- type: z.string()
1244
+ type: z.string(),
1245
+ avatarColorToken: z.string().nullable(),
1246
+ avatarImageUrl: z.string().nullable()
1170
1247
  });
1248
+ /**
1249
+ * Live activity hint surfaced in the conversation row's time slot. Derived
1250
+ * server-side from the latest `session_events` row for the chat. See
1251
+ * `MeChatRow.liveActivity` for the lifecycle rules.
1252
+ *
1253
+ * `kind` is intentionally narrower than the full `sessionEventKind` enum:
1254
+ * `turn_end` / `error` produce `liveActivity: null` rather than a live
1255
+ * indicator.
1256
+ */
1257
+ const liveActivityKindSchema = z.enum([
1258
+ "tool_call",
1259
+ "thinking",
1260
+ "assistant_text"
1261
+ ]);
1262
+ const liveActivitySchema = z.object({
1263
+ agentId: z.string(),
1264
+ kind: liveActivityKindSchema,
1265
+ label: z.string(),
1266
+ startedAt: z.string()
1267
+ });
1268
+ /** Stale threshold (ms) past which a `session_events` row stops driving liveActivity. */
1269
+ const LIVE_ACTIVITY_STALE_MS = 6e4;
1171
1270
  const meChatRowSchema = z.object({
1172
1271
  chatId: z.string(),
1173
1272
  type: z.string(),
@@ -1181,7 +1280,8 @@ const meChatRowSchema = z.object({
1181
1280
  unreadMentionCount: z.number().int(),
1182
1281
  canReply: z.boolean(),
1183
1282
  engagementStatus: chatEngagementStatusSchema,
1184
- workingAgentIds: z.array(z.string())
1283
+ engagedAgentIds: z.array(z.string()),
1284
+ liveActivity: liveActivitySchema.nullable()
1185
1285
  });
1186
1286
  z.object({
1187
1287
  rows: z.array(meChatRowSchema),
@@ -1205,6 +1305,47 @@ z.object({
1205
1305
  type: z.literal("chat:message"),
1206
1306
  chatId: z.string()
1207
1307
  });
1308
+ /**
1309
+ * Per-source aggregate for the conversation-list tag bar.
1310
+ *
1311
+ * - `chatCount` — number of chats the caller is in for this source. Used
1312
+ * to hide tags whose count is 0 ("don't render a PR tag if there are no
1313
+ * PRs").
1314
+ * - `unreadChatCount` — number of chats whose `unread_mention_count > 0`.
1315
+ * This is "chats with unread mentions", NOT "total mention count", so
1316
+ * the badge on each tag matches the semantics of the existing `unread`
1317
+ * filter pill (`totalUnread` in `pages/workspace/conversations`) — a
1318
+ * `2` on the PR tag means "2 PR chats have unread mentions", which is
1319
+ * what a user expects to click into.
1320
+ *
1321
+ * The map ALWAYS contains the `manual` key (the default tab is always
1322
+ * available, even at zero counts); other keys are present only when the
1323
+ * caller has at least one chat for that source.
1324
+ */
1325
+ const chatSourceCountSchema = z.object({
1326
+ chatCount: z.number().int().nonnegative(),
1327
+ unreadChatCount: z.number().int().nonnegative()
1328
+ });
1329
+ const listMeChatSourceCountsQuerySchema = z.object({ engagement: chatEngagementViewSchema.default("active") });
1330
+ z.object({ counts: z.partialRecord(chatSourceSchema, chatSourceCountSchema) });
1331
+ const workspaceDocRefSchema = z.object({
1332
+ type: z.literal("workspace"),
1333
+ chatId: z.string().trim().min(1),
1334
+ agentId: z.string().trim().min(1),
1335
+ basePath: z.string().trim().optional(),
1336
+ path: z.string().trim().min(1)
1337
+ });
1338
+ z.object({ basePath: z.string().trim().min(1) });
1339
+ const getMeDocSchema = z.object({
1340
+ agentId: z.string().trim().min(1),
1341
+ basePath: z.string().trim().optional(),
1342
+ path: z.string().trim().min(1)
1343
+ });
1344
+ const getMeDocResponseSchema = z.object({
1345
+ ref: workspaceDocRefSchema,
1346
+ path: z.string(),
1347
+ content: z.string()
1348
+ });
1208
1349
  z.enum([
1209
1350
  "connect",
1210
1351
  "create_agent",
@@ -1263,7 +1404,8 @@ z.object({
1263
1404
  organizationId: z.string(),
1264
1405
  organizationName: z.string(),
1265
1406
  role: z.enum(["admin", "member"]),
1266
- agentId: z.string()
1407
+ agentId: z.string(),
1408
+ orgHasOtherMembers: z.boolean()
1267
1409
  });
1268
1410
  const memberRoleSchema = z.enum(["admin", "member"]);
1269
1411
  const memberSchema = z.object({
@@ -1674,7 +1816,8 @@ const sessionEventKind = z.enum([
1674
1816
  "error",
1675
1817
  "assistant_text",
1676
1818
  "thinking",
1677
- "turn_end"
1819
+ "turn_end",
1820
+ "context_tree_usage"
1678
1821
  ]);
1679
1822
  const toolCallEventPayload = z.object({
1680
1823
  toolUseId: z.string(),
@@ -1716,6 +1859,10 @@ const thinkingEventPayload = z.object({});
1716
1859
  * completed turns to show only the final result message.
1717
1860
  */
1718
1861
  const turnEndEventPayload = z.object({ status: z.enum(["success", "error"]) });
1862
+ const contextTreeUsageEventPayload = z.object({
1863
+ purpose: z.literal("design_decision"),
1864
+ treeRepoUrl: z.string().nullable()
1865
+ });
1719
1866
  const sessionEventSchema = z.discriminatedUnion("kind", [
1720
1867
  z.object({
1721
1868
  kind: z.literal("tool_call"),
@@ -1736,6 +1883,10 @@ const sessionEventSchema = z.discriminatedUnion("kind", [
1736
1883
  z.object({
1737
1884
  kind: z.literal("turn_end"),
1738
1885
  payload: turnEndEventPayload
1886
+ }),
1887
+ z.object({
1888
+ kind: z.literal("context_tree_usage"),
1889
+ payload: contextTreeUsageEventPayload
1739
1890
  })
1740
1891
  ]);
1741
1892
  z.object({
@@ -1749,7 +1900,8 @@ z.object({
1749
1900
  errorEventPayload,
1750
1901
  assistantTextEventPayload,
1751
1902
  thinkingEventPayload,
1752
- turnEndEventPayload
1903
+ turnEndEventPayload,
1904
+ contextTreeUsageEventPayload
1753
1905
  ]),
1754
1906
  createdAt: z.string()
1755
1907
  });
@@ -1826,4 +1978,4 @@ z.object({
1826
1978
  capabilities: serverCapabilitiesSchema.optional()
1827
1979
  }).passthrough();
1828
1980
  //#endregion
1829
- export { onboardingEventSchema as $, createOrgFromMeSchema as A, githubStartQuerySchema as B, contextTreeSnapshotSchema as C, updateClientCapabilitiesSchema as Ct, createChatSchema as D, createAgentSchema as E, wsAuthFrameSchema as Et, extractMentions as F, isOrgSettingNamespace as G, inboxAckFrameSchema as H, githubAppInstallationClaimBodySchema as I, joinByInvitationSchema as J, isRedactedEnvValue as K, githubAppInstallationPermissionsSchema as L, defaultRuntimeConfigPayload as M, delegateFeishuUserSchema as N, createMeChatSchema as O, dryRunAgentRuntimeConfigSchema as P, notificationQuerySchema as Q, githubCallbackQuerySchema as R, connectTokenExchangeSchema as S, updateChatSchema as St, createAdapterMappingSchema as T, updateOrganizationSchema as Tt, inboxDeliverFrameSchema as U, imageInlineContentSchema as V, inboxPollQuerySchema as W, loginSchema as X, listMeChatsQuerySchema as Y, messageSourceSchema as Z, agentRuntimeConfigPayloadSchema as _, stripCode as _t, AGENT_TYPES as a, rebindAgentSchema as at, clientCapabilitiesSchema as b, updateAgentRuntimeConfigSchema as bt, DEFAULT_RUNTIME_PROVIDER as c, safeRedirectPath as ct, ORG_SETTINGS_NAMESPACES as d, sendMessageSchema as dt, paginationQuerySchema as et, WS_AUTH_FRAME_TIMEOUT_MS as f, sendToAgentSchema as ft, agentPinnedMessageSchema as g, sessionStateMessageSchema as gt, agentBindRequestSchema as h, sessionReconcileRequestSchema as ht, AGENT_STATUSES as i, questionMessageContentSchema as it, defaultParticipantMode as j, createMemberSchema as k, MENTION_REGEX as l, scanMentionTokens as lt, addParticipantSchema as m, sessionEventSchema as mt, AGENT_NAME_REGEX as n, patchOnboardingSchema as nt, AGENT_VISIBILITY as o, refreshTokenSchema as ot, addMeChatParticipantsSchema as p, sessionEventMessageSchema as pt, isReservedAgentName as q, AGENT_SELECTOR_HEADER as r, questionAnswerMessageContentSchema as rt, CHAT_ENGAGEMENT_STATUSES as s, runtimeStateMessageSchema as st, AGENT_BIND_REJECT_REASONS as t, patchChatEngagementSchema as tt, NOTIFICATION_TYPES as u, selfServiceFeishuBotSchema as ut, agentTypeSchema as v, submitQuestionAnswerSchema as vt, createAdapterConfigSchema as w, updateMemberSchema as wt, clientRegisterSchema as x, updateAgentSchema as xt, chatMetadataSchema as y, updateAdapterConfigSchema as yt, githubDevCallbackQuerySchema as z };
1981
+ export { listMeChatSourceCountsQuerySchema as $, createMeChatSchema as A, updateOrganizationSchema as At, githubAppInstallationClaimBodySchema as B, clientRegisterSchema as C, submitQuestionAnswerSchema as Ct, createAdapterMappingSchema as D, updateChatSchema as Dt, createAdapterConfigSchema as E, updateAgentSchema as Et, delegateFeishuUserSchema as F, imageInlineContentSchema as G, githubCallbackQuerySchema as H, dryRunAgentRuntimeConfigSchema as I, inboxPollQuerySchema as J, inboxAckFrameSchema as K, extractMentions as L, createOrgFromMeSchema as M, defaultParticipantMode as N, createAgentSchema as O, updateClientCapabilitiesSchema as Ot, defaultRuntimeConfigPayload as P, joinByInvitationSchema as Q, getMeDocResponseSchema as R, clientCapabilitiesSchema as S, stripCode as St, contextTreeSnapshotSchema as T, updateAgentRuntimeConfigSchema as Tt, githubDevCallbackQuerySchema as U, githubAppInstallationPermissionsSchema as V, githubStartQuerySchema as W, isRedactedEnvValue as X, isOrgSettingNamespace as Y, isReservedAgentName as Z, agentBindRequestSchema as _, sendToAgentSchema as _t, AGENT_TYPES as a, paginationQuerySchema as at, agentTypeSchema as b, sessionReconcileRequestSchema as bt, DEFAULT_RUNTIME_PROVIDER as c, questionAnswerMessageContentSchema as ct, MENTION_REGEX as d, refreshTokenSchema as dt, listMeChatsQuerySchema as et, NOTIFICATION_TYPES as f, runtimeStateMessageSchema as ft, addParticipantSchema as g, sendMessageSchema as gt, addMeChatParticipantsSchema as h, selfServiceFeishuBotSchema as ht, AGENT_STATUSES as i, onboardingEventSchema as it, createMemberSchema as j, wsAuthFrameSchema as jt, createChatSchema as k, updateMemberSchema as kt, GITHUB_ENTITY_TYPES as l, questionMessageContentSchema as lt, WS_AUTH_FRAME_TIMEOUT_MS as m, scanMentionTokens as mt, AGENT_NAME_REGEX as n, messageSourceSchema as nt, AGENT_VISIBILITY as o, patchChatEngagementSchema as ot, ORG_SETTINGS_NAMESPACES as p, safeRedirectPath as pt, inboxDeliverFrameSchema as q, AGENT_SELECTOR_HEADER as r, notificationQuerySchema as rt, CHAT_ENGAGEMENT_STATUSES as s, patchOnboardingSchema as st, AGENT_BIND_REJECT_REASONS as t, loginSchema as tt, LIVE_ACTIVITY_STALE_MS as u, rebindAgentSchema as ut, agentPinnedMessageSchema as v, sessionEventMessageSchema as vt, connectTokenExchangeSchema as w, updateAdapterConfigSchema as wt, chatMetadataSchema as x, sessionStateMessageSchema as xt, agentRuntimeConfigPayloadSchema as y, sessionEventSchema as yt, getMeDocSchema as z };
@@ -0,0 +1,32 @@
1
+ -- Onboarding terminal-state stamp. Distinct from `onboarding_dismissed_at`:
2
+ --
3
+ -- * `onboarding_dismissed_at` means "the user clicked ✕ on the stepper" —
4
+ -- a UI hide, not a setup-complete signal. The user can resume the
5
+ -- wizard from Settings → Onboarding to land back on whichever step is
6
+ -- still incomplete.
7
+ --
8
+ -- * `onboarding_completed_at` means "the user actually walked Step 3 to
9
+ -- success" (admin Continue, invitee Confirm/Continue). Once set, the
10
+ -- Settings → Onboarding entry point and Resume button disappear
11
+ -- permanently. Subsequent tree / source-repo edits go through Settings
12
+ -- → Team and /agents/:uuid.
13
+ --
14
+ -- The two stamps are intentionally orthogonal: `inferOnboardingStep()`
15
+ -- keeps its existing "infer from current resources" semantics and never
16
+ -- consults this column. This field is UI-gate only.
17
+ --
18
+ -- Backfill: every already-dismissed user is treated as completed. The pre-
19
+ -- column population (mid-2025) had no terminal-state concept, so the only
20
+ -- signal we have is "they hid the stepper" — and the alternative (showing
21
+ -- the Onboarding sidebar entry to every legacy user forever) is worse than
22
+ -- the off-by-one risk that a few users dismissed-without-finishing and
23
+ -- will lose their Resume affordance.
24
+
25
+ ALTER TABLE "users"
26
+ ADD COLUMN IF NOT EXISTS "onboarding_completed_at" timestamp with time zone;
27
+
28
+ --> statement-breakpoint
29
+ UPDATE "users"
30
+ SET "onboarding_completed_at" = "onboarding_dismissed_at"
31
+ WHERE "onboarding_dismissed_at" IS NOT NULL
32
+ AND "onboarding_completed_at" IS NULL;
@@ -0,0 +1,11 @@
1
+ -- Add avatar_color_token to agents.
2
+ --
3
+ -- Manager-selected avatar fill color override for the web client. One of
4
+ -- "hue-0".."hue-7", matching the `--avatar-hue-*` CSS tokens in
5
+ -- `packages/web/src/index.css`. NULL means "auto" — the renderer falls
6
+ -- back to the deterministic djb2 hash of `uuid` (the previous default).
7
+ --
8
+ -- Nullable, no default, no backfill: existing rows continue to render the
9
+ -- hashed color until their manager picks something explicit. This is a
10
+ -- pure additive change; no NOT NULL constraint, no enum, no FK.
11
+ ALTER TABLE "agents" ADD COLUMN "avatar_color_token" text;
@@ -0,0 +1,17 @@
1
+ -- Add inline avatar image storage to agents.
2
+ --
3
+ -- The PRD chose PG bytea over object storage for the first cut: clients
4
+ -- always pre-resize to 256×256 WEBP (typically < 50 KB), and a few KB ×
5
+ -- N agents per org sits well within row-size budgets. Switching to S3/R2
6
+ -- later is a follow-up migration; the column shape stays the same except
7
+ -- avatar_image_data flips to NULL and avatar_image_url moves to a real
8
+ -- external URL.
9
+ --
10
+ -- All three columns are nullable and move together — either all three
11
+ -- carry an image (data + mime + updated_at) or all are NULL. The service
12
+ -- layer enforces that invariant on writes; SQL keeps it loose so a partial
13
+ -- backfill / restore doesn't break inserts.
14
+ ALTER TABLE "agents"
15
+ ADD COLUMN "avatar_image_data" bytea,
16
+ ADD COLUMN "avatar_image_mime" text,
17
+ ADD COLUMN "avatar_image_updated_at" timestamp with time zone;
@@ -302,6 +302,27 @@
302
302
  "when": 1779408000000,
303
303
  "tag": "0042_notifications_drop_legacy_types",
304
304
  "breakpoints": true
305
+ },
306
+ {
307
+ "idx": 43,
308
+ "version": "7",
309
+ "when": 1779494400000,
310
+ "tag": "0043_onboarding_completed_at",
311
+ "breakpoints": true
312
+ },
313
+ {
314
+ "idx": 44,
315
+ "version": "7",
316
+ "when": 1779580800000,
317
+ "tag": "0044_agent_avatar_color",
318
+ "breakpoints": true
319
+ },
320
+ {
321
+ "idx": 45,
322
+ "version": "7",
323
+ "when": 1779667200000,
324
+ "tag": "0045_agent_avatar_image",
325
+ "breakpoints": true
305
326
  }
306
327
  ]
307
328
  }
@@ -1,5 +1,5 @@
1
1
  import { integer, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
2
- //#region ../server/dist/errors-CF5evtJt.mjs
2
+ //#region ../server/dist/errors-LPcARA4K.mjs
3
3
  /** Organization entity. Agents and chats belong to exactly one organization. */
4
4
  const organizations = pgTable("organizations", {
5
5
  id: text("id").primaryKey(),
@@ -20,6 +20,7 @@ const users = pgTable("users", {
20
20
  avatarUrl: text("avatar_url"),
21
21
  status: text("status").notNull().default("active"),
22
22
  onboardingDismissedAt: timestamp("onboarding_dismissed_at", { withTimezone: true }),
23
+ onboardingCompletedAt: timestamp("onboarding_completed_at", { withTimezone: true }),
23
24
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
24
25
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
25
26
  });
@@ -1,6 +1,6 @@
1
1
  import { r as __exportAll } from "./chunk-BSw8zbkd.mjs";
2
2
  import { t as cliFetch } from "./cli-fetch--tiwKm5S.mjs";
3
- import { r as AGENT_SELECTOR_HEADER } from "./dist-B1GHzMLc.mjs";
3
+ import { r as AGENT_SELECTOR_HEADER } from "./dist-1XGLJMOq.mjs";
4
4
  //#region src/core/feishu.ts
5
5
  var feishu_exports = /* @__PURE__ */ __exportAll({
6
6
  bindFeishuBot: () => bindFeishuBot,
package/dist/index.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  import "./observability-BAScT_5S-BcW9HgkG.mjs";
2
- import { A as checkDocker, B as isServiceSupported, E as checkAgentConfigs, F as checkWebSocket, G as uninstallClientService, H as restartClientService, I as printResults, J as stopPostgres, K as ensurePostgres, M as checkServerConfig, N as checkServerHealth, O as checkClientConfig, P as checkServerReachable, R as getClientServiceStatus, S as runHomeMigration, T as runMigrations, U as startClientService, V as resolveCliInvocation, W as stopClientService, X as handleClientOrgMismatch, Y as ClientRuntime, Z as rotateClientIdWithBackup, _ as formatCheckReport, b as onboardCreate, d as startServer, g as promptMissingFields, h as promptAddAgent, j as checkNodeVersion, k as checkDatabase, lt as FirstTreeHubSDK, m as isInteractive, n as deriveHubUrlFromToken, nt as hasUser, q as isDockerAvailable, t as HubUrlDerivationError, tt as createOwner, ut as SdkError, y as onboardCheck, z as installClientService } from "./saas-connect-Fgnnnola.mjs";
2
+ import { A as checkDocker, B as isServiceSupported, E as checkAgentConfigs, F as checkWebSocket, G as uninstallClientService, H as restartClientService, I as printResults, J as stopPostgres, K as ensurePostgres, M as checkServerConfig, N as checkServerHealth, O as checkClientConfig, P as checkServerReachable, R as getClientServiceStatus, S as runHomeMigration, T as runMigrations, U as startClientService, V as resolveCliInvocation, W as stopClientService, X as handleClientOrgMismatch, Y as ClientRuntime, Z as rotateClientIdWithBackup, _ as formatCheckReport, b as onboardCreate, d as startServer, g as promptMissingFields, h as promptAddAgent, j as checkNodeVersion, k as checkDatabase, lt as FirstTreeHubSDK, m as isInteractive, n as deriveHubUrlFromToken, nt as hasUser, q as isDockerAvailable, t as HubUrlDerivationError, tt as createOwner, ut as SdkError, y as onboardCheck, z as installClientService } from "./saas-connect-DX3-nDs9.mjs";
3
3
  import "./logger-core-BTmvdflj-DjW8FM4T.mjs";
4
- import { a as ensureFreshAdminToken, c as resolveServerUrl, i as ensureFreshAccessToken, n as AuthRefreshRateLimitedError, s as resolveAccessToken, t as AuthRefreshFailedError } from "./bootstrap-CW73oEYn.mjs";
4
+ import { a as ensureFreshAdminToken, c as resolveServerUrl, i as ensureFreshAccessToken, n as AuthRefreshRateLimitedError, s as resolveAccessToken, t as AuthRefreshFailedError } from "./bootstrap-C15ZBOCC.mjs";
5
5
  import { i as blank, s as status } from "./cli-fetch--tiwKm5S.mjs";
6
- import "./dist-B1GHzMLc.mjs";
7
- import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-30vUx69l.mjs";
8
- import "./errors-CF5evtJt-B0NTIVPt.mjs";
6
+ import "./dist-1XGLJMOq.mjs";
7
+ import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-BGx71p5s.mjs";
8
+ import "./errors-LPcARA4K-Dbrptiyz.mjs";
9
9
  import "./src-DNBS5Yjj.mjs";
10
- import "./client-BViGcaUC-CZb2Svgh.mjs";
11
- import "./invitation-Bg0TRiyx-BsZH4GCS.mjs";
10
+ import "./client-RM_03B_l-DiEIa9xe.mjs";
11
+ import "./invitation-DZO4NX3P-BPxTeHf-.mjs";
12
12
  export { AuthRefreshFailedError, AuthRefreshRateLimitedError, ClientRuntime, FirstTreeHubSDK, HubUrlDerivationError, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, deriveHubUrlFromToken, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, getClientServiceStatus, handleClientOrgMismatch, hasUser, installClientService, isDockerAvailable, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, restartClientService, rotateClientIdWithBackup, runHomeMigration, runMigrations, startClientService, startServer, status, stopClientService, stopPostgres, uninstallClientService };
@@ -0,0 +1,4 @@
1
+ import "./dist-1XGLJMOq.mjs";
2
+ import "./errors-LPcARA4K-Dbrptiyz.mjs";
3
+ import { s as previewInvitation } from "./invitation-DZO4NX3P-BPxTeHf-.mjs";
4
+ export { previewInvitation };
@@ -1,8 +1,8 @@
1
- import { l as organizations, s as NotFoundError, u as users } from "./errors-CF5evtJt-B0NTIVPt.mjs";
1
+ import { l as organizations, s as NotFoundError, u as users } from "./errors-LPcARA4K-Dbrptiyz.mjs";
2
2
  import { randomBytes } from "node:crypto";
3
3
  import { and, desc, eq, gt, isNull, or } from "drizzle-orm";
4
4
  import { index, pgTable, text, timestamp } from "drizzle-orm/pg-core";
5
- //#region ../server/dist/invitation-Bg0TRiyx.mjs
5
+ //#region ../server/dist/invitation-DZO4NX3P.mjs
6
6
  /** Generate a UUID v7 (time-ordered). No external dependency. */
7
7
  function uuidv7() {
8
8
  const now = BigInt(Date.now());