@agent-team-foundation/first-tree-hub 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  import "../logger-core-2yeIU1fc-B-__AsQO.mjs";
3
3
  import { C as serverConfigSchema, _ as loadAgents, b as resetConfig, c as saveCredentials, f as agentConfigSchema, g as initConfig, h as getConfigValue, i as loadCredentials, l as DEFAULT_CONFIG_DIR, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, r as ensureFreshAdminToken, s as saveAgentConfig, u as DEFAULT_DATA_DIR, w as setConfigValue, x as resetConfigMeta, y as readConfigFile } from "../bootstrap-CWcBzk6C.mjs";
4
4
  import "../observability-CJzDFY_G-CmvgUuzc.mjs";
5
- import { A as printResults, B as SdkError, C as checkDatabase, D as checkServerHealth, E as checkServerConfig, F as stopPostgres, H as cleanWorkspaces, I as ClientRuntime, L as createOwner, O as checkServerReachable, S as checkClientConfig, T as checkNodeVersion, U as applyClientLoggerConfig, V as SessionRegistry, _ as isServiceSupported, a as COMMAND_VERSION, b as runMigrations, c as promptMissingFields, d as onboardCheck, f as onboardCreate, g as installClientService, h as getClientServiceStatus, i as startServer, k as checkWebSocket, l as formatCheckReport, m as runHomeMigration, n as declineUpdate, o as isInteractive, p as saveOnboardState, r as promptUpdate, s as promptAddAgent, t as createExecuteUpdate, u as loadOnboardState, w as checkDocker, x as checkAgentConfigs, y as uninstallClientService, z as FirstTreeHubSDK } from "../core-DZDhomaN.mjs";
5
+ import { A as printResults, B as SdkError, C as checkDatabase, D as checkServerHealth, E as checkServerConfig, F as stopPostgres, H as cleanWorkspaces, I as ClientRuntime, L as createOwner, O as checkServerReachable, S as checkClientConfig, T as checkNodeVersion, U as applyClientLoggerConfig, V as SessionRegistry, _ as isServiceSupported, a as COMMAND_VERSION, b as runMigrations, c as promptMissingFields, d as onboardCheck, f as onboardCreate, g as installClientService, h as getClientServiceStatus, i as startServer, k as checkWebSocket, l as formatCheckReport, m as runHomeMigration, n as declineUpdate, o as isInteractive, p as saveOnboardState, r as promptUpdate, s as promptAddAgent, t as createExecuteUpdate, u as loadOnboardState, w as checkDocker, x as checkAgentConfigs, y as uninstallClientService, z as FirstTreeHubSDK } from "../core-DzuW7b5v.mjs";
6
6
  import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-GlaczcVf.mjs";
7
7
  import { Command } from "commander";
8
8
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
@@ -7807,7 +7807,7 @@ var require_secure_json_parse = /* @__PURE__ */ __commonJSMin(((exports, module)
7807
7807
  module.exports.scan = filter;
7808
7808
  }));
7809
7809
  //#endregion
7810
- //#region ../server/dist/app-BDvasXc4.mjs
7810
+ //#region ../server/dist/app-BcKNAbK-.mjs
7811
7811
  var import_multipart = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
7812
7812
  const Busboy = require_main();
7813
7813
  const os = __require("node:os");
@@ -11111,17 +11111,35 @@ async function createNotification(db, data) {
11111
11111
  pushToWebhook(db, notification).catch(() => {});
11112
11112
  return notification;
11113
11113
  }
11114
- /** List notifications with pagination and optional filters. */
11115
- async function listNotifications(db, orgId, query) {
11114
+ /**
11115
+ * List notifications with pagination and optional filters, scoped to the
11116
+ * caller's visible agents.
11117
+ *
11118
+ * Rule: a member sees a notification iff
11119
+ * - it carries an `agentId` the member can see
11120
+ * (`agents.visibility = organization` OR `agents.managerId = self`), OR
11121
+ * - it has no `agentId` (org-wide system notification)
11122
+ *
11123
+ * Private agents owned by other members never surface.
11124
+ */
11125
+ async function listNotifications(db, orgId, memberId, query) {
11126
+ const visibleAgents = await loadVisibleAgentIds$1(db, orgId, memberId);
11127
+ if (query.agentId && !visibleAgents.has(query.agentId)) return {
11128
+ items: [],
11129
+ nextCursor: null
11130
+ };
11116
11131
  const conditions = [eq(notifications.organizationId, orgId)];
11117
11132
  if (query.cursor) conditions.push(lt(notifications.createdAt, new Date(query.cursor)));
11118
11133
  if (query.severity) conditions.push(eq(notifications.severity, query.severity));
11119
11134
  if (query.read !== void 0) conditions.push(eq(notifications.read, query.read));
11120
11135
  if (query.agentId) conditions.push(eq(notifications.agentId, query.agentId));
11121
11136
  const where = and(...conditions);
11122
- const rows = await db.select().from(notifications).where(where).orderBy(desc(notifications.createdAt)).limit(query.limit + 1);
11123
- const hasMore = rows.length > query.limit;
11124
- const items = hasMore ? rows.slice(0, query.limit) : rows;
11137
+ const overscanFactor = 4;
11138
+ const targetLimit = query.limit;
11139
+ const rawLimit = Math.min(targetLimit * overscanFactor + 1, 400);
11140
+ const visible = (await db.select().from(notifications).where(where).orderBy(desc(notifications.createdAt)).limit(rawLimit)).filter((n) => n.agentId === null || visibleAgents.has(n.agentId));
11141
+ const hasMore = visible.length > targetLimit;
11142
+ const items = hasMore ? visible.slice(0, targetLimit) : visible;
11125
11143
  const last = items[items.length - 1];
11126
11144
  const nextCursor = hasMore && last ? last.createdAt.toISOString() : null;
11127
11145
  return {
@@ -11132,36 +11150,116 @@ async function listNotifications(db, orgId, query) {
11132
11150
  nextCursor
11133
11151
  };
11134
11152
  }
11135
- /** Mark a single notification as read, scoped to organization. */
11136
- async function markRead(db, notificationId, organizationId) {
11137
- const [updated] = await db.update(notifications).set({ read: true }).where(and(eq(notifications.id, notificationId), eq(notifications.organizationId, organizationId))).returning();
11153
+ /** Mark a single notification as read, scoped to organization + visible agents. */
11154
+ async function markRead(db, notificationId, orgId, memberId) {
11155
+ const [existing] = await db.select({
11156
+ id: notifications.id,
11157
+ agentId: notifications.agentId
11158
+ }).from(notifications).where(and(eq(notifications.id, notificationId), eq(notifications.organizationId, orgId))).limit(1);
11159
+ if (!existing) return null;
11160
+ if (existing.agentId) {
11161
+ if (!(await loadVisibleAgentIds$1(db, orgId, memberId)).has(existing.agentId)) return null;
11162
+ }
11163
+ const [updated] = await db.update(notifications).set({ read: true }).where(and(eq(notifications.id, notificationId), eq(notifications.organizationId, orgId))).returning();
11138
11164
  return updated ?? null;
11139
11165
  }
11140
- /** Mark all notifications as read for an organization. */
11141
- async function markAllRead(db, orgId) {
11142
- await db.update(notifications).set({ read: true }).where(and(eq(notifications.organizationId, orgId), eq(notifications.read, false)));
11166
+ /** Mark all notifications visible to this member as read. */
11167
+ async function markAllRead(db, orgId, memberId) {
11168
+ const visible = await loadVisibleAgentIds$1(db, orgId, memberId);
11169
+ const idsToMark = (await db.select({
11170
+ id: notifications.id,
11171
+ agentId: notifications.agentId
11172
+ }).from(notifications).where(and(eq(notifications.organizationId, orgId), eq(notifications.read, false))).limit(1e3)).filter((n) => n.agentId === null || visible.has(n.agentId)).map((n) => n.id);
11173
+ if (idsToMark.length === 0) return;
11174
+ const batchSize = 200;
11175
+ for (let i = 0; i < idsToMark.length; i += batchSize) {
11176
+ const batch = idsToMark.slice(i, i + batchSize);
11177
+ await db.update(notifications).set({ read: true }).where(and(eq(notifications.organizationId, orgId), eq(notifications.read, false), inArray(notifications.id, batch)));
11178
+ }
11143
11179
  }
11144
11180
  /**
11145
- * Convenience: create a notification for an agent event, resolving org automatically.
11146
- * Fire-and-forget — errors are swallowed.
11181
+ * Shared visibility predicate. Mirrors
11182
+ * {@link packages/server/src/services/access-control.ts#agentVisibilityCondition}
11183
+ * but returns a Set because the notification query joins are mostly in Node.
11147
11184
  */
11148
- async function notifyAgentEvent(db, agentId, type, severity, message, chatId) {
11185
+ async function loadVisibleAgentIds$1(db, orgId, memberId) {
11186
+ const rows = await db.select({ id: agents.uuid }).from(agents).where(and(eq(agents.organizationId, orgId), ne(agents.status, AGENT_STATUSES.DELETED), or(eq(agents.visibility, AGENT_VISIBILITY.ORGANIZATION), eq(agents.managerId, memberId))));
11187
+ return new Set(rows.map((r) => r.id));
11188
+ }
11189
+ async function resolveAgentContext(db, agentId) {
11190
+ const [agent] = await db.select({
11191
+ organizationId: agents.organizationId,
11192
+ name: agents.name,
11193
+ displayName: agents.displayName,
11194
+ clientId: agents.clientId
11195
+ }).from(agents).where(eq(agents.uuid, agentId)).limit(1);
11196
+ if (!agent) return null;
11197
+ let clientLabel = null;
11198
+ if (agent.clientId) {
11199
+ const [client] = await db.select({
11200
+ hostname: clients.hostname,
11201
+ id: clients.id
11202
+ }).from(clients).where(eq(clients.id, agent.clientId)).limit(1);
11203
+ clientLabel = client?.hostname ?? agent.clientId;
11204
+ }
11205
+ return {
11206
+ organizationId: agent.organizationId,
11207
+ agentName: agent.displayName ?? agent.name ?? agentId,
11208
+ clientId: agent.clientId,
11209
+ clientLabel
11210
+ };
11211
+ }
11212
+ async function resolveChatContext(db, chatId) {
11213
+ const [chat] = await db.select({ topic: chats.topic }).from(chats).where(eq(chats.id, chatId)).limit(1);
11214
+ const shortId = chatId.slice(0, 8);
11215
+ return { chatLabel: chat?.topic && chat.topic.trim().length > 0 ? chat.topic.trim() : `Chat ${shortId}` };
11216
+ }
11217
+ /**
11218
+ * Compose a human-readable message for each notification type.
11219
+ *
11220
+ * Keep subjects consistent with what the dashboard shows the member:
11221
+ * - Session-scoped events → subject is the chat (topic / "Chat xxxxxxxx")
11222
+ * - Client-scoped events → subject is the computer (hostname / clientId)
11223
+ * - Agent-scoped events → subject is the agent display name
11224
+ */
11225
+ function composeMessage(type, agentCtx, chatCtx) {
11226
+ const agent = agentCtx.agentName;
11227
+ const computer = agentCtx.clientLabel ?? "Unknown computer";
11228
+ const chat = chatCtx?.chatLabel ?? null;
11229
+ switch (type) {
11230
+ case "session_completed": return chat ? `${chat} completed` : `${agent} completed a task`;
11231
+ case "session_error": return chat ? `${chat} hit an error` : `${agent} hit a session error`;
11232
+ case "agent_disconnected": return `Computer ${computer} disconnected`;
11233
+ case "agent_connected": return `Computer ${computer} reconnected`;
11234
+ case "agent_stale": return `Computer ${computer} is unresponsive`;
11235
+ case "agent_error": return `${agent} entered error state`;
11236
+ case "agent_blocked": return `${agent} is blocked`;
11237
+ case "agent_needs_decision": return chat ? `${agent} needs a decision in ${chat}` : `${agent} needs a decision`;
11238
+ default: return `${agent} event`;
11239
+ }
11240
+ }
11241
+ /**
11242
+ * Convenience: create a notification for an agent event, resolving org,
11243
+ * agent display name, computer hostname, and chat topic automatically.
11244
+ * Callers supply the event type and severity; the message text is generated
11245
+ * here so language/phrasing is centralized (see {@link composeMessage}).
11246
+ *
11247
+ * Fire-and-forget — errors are swallowed so event producers never fail just
11248
+ * because the notification pipeline is unhealthy.
11249
+ */
11250
+ async function notifyAgentEvent(db, agentId, type, severity, chatId) {
11149
11251
  try {
11150
- const [agent] = await db.select({
11151
- organizationId: agents.organizationId,
11152
- name: agents.name,
11153
- displayName: agents.displayName
11154
- }).from(agents).where(eq(agents.uuid, agentId)).limit(1);
11155
- if (!agent) return;
11156
- const name = agent.displayName ?? agent.name ?? agentId;
11157
- const resolvedMessage = message.replace(agentId, name);
11252
+ const agentCtx = await resolveAgentContext(db, agentId);
11253
+ if (!agentCtx) return;
11254
+ const message = composeMessage(type, agentCtx, chatId ? await resolveChatContext(db, chatId) : null);
11158
11255
  await createNotification(db, {
11159
- organizationId: agent.organizationId,
11256
+ organizationId: agentCtx.organizationId,
11160
11257
  type,
11161
11258
  severity,
11162
11259
  agentId,
11163
11260
  chatId: chatId ?? null,
11164
- message: resolvedMessage
11261
+ clientId: agentCtx.clientId,
11262
+ message
11165
11263
  });
11166
11264
  } catch {}
11167
11265
  }
@@ -11169,6 +11267,7 @@ function pushToAdminWs(notification) {
11169
11267
  broadcastToAdmins({
11170
11268
  type: "notification",
11171
11269
  organizationId: notification.organizationId,
11270
+ agentId: notification.agentId ?? null,
11172
11271
  data: notification
11173
11272
  });
11174
11273
  }
@@ -11185,26 +11284,34 @@ async function pushToWebhook(db, notification) {
11185
11284
  } catch {}
11186
11285
  }
11187
11286
  async function adminNotificationRoutes(app) {
11188
- /** GET /admin/notifications — list notifications scoped to caller's org */
11287
+ /**
11288
+ * GET /admin/notifications — list notifications visible to the caller.
11289
+ *
11290
+ * Scoped by (a) organization (via JWT) and (b) per-agent visibility: the
11291
+ * member only sees notifications whose agentId is visible to them
11292
+ * (organization-visible agents or agents they manage), plus org-wide
11293
+ * system notifications with no agentId. This mirrors the rule the admin
11294
+ * WebSocket route enforces on live pushes — REST and WS stay in sync.
11295
+ */
11189
11296
  app.get("/", async (request) => {
11190
11297
  const member = requireMember(request);
11191
11298
  const query = notificationQuerySchema.parse(request.query);
11192
- return listNotifications(app.db, member.organizationId, query);
11299
+ return listNotifications(app.db, member.organizationId, member.memberId, query);
11193
11300
  });
11194
11301
  /** POST /admin/notifications/:id/read — mark a single notification as read */
11195
11302
  app.post("/:id/read", async (request) => {
11196
11303
  const member = requireMember(request);
11197
- const result = await markRead(app.db, request.params.id, member.organizationId);
11304
+ const result = await markRead(app.db, request.params.id, member.organizationId, member.memberId);
11198
11305
  if (!result) throw new NotFoundError(`Notification "${request.params.id}" not found`);
11199
11306
  return {
11200
11307
  ...result,
11201
11308
  createdAt: result.createdAt.toISOString()
11202
11309
  };
11203
11310
  });
11204
- /** POST /admin/notifications/read-all — mark all notifications as read */
11311
+ /** POST /admin/notifications/read-all — mark all visible notifications as read */
11205
11312
  app.post("/read-all", async (request) => {
11206
11313
  const member = requireMember(request);
11207
- await markAllRead(app.db, member.organizationId);
11314
+ await markAllRead(app.db, member.organizationId, member.memberId);
11208
11315
  return { status: "ok" };
11209
11316
  });
11210
11317
  }
@@ -12315,6 +12422,8 @@ function adminWsRoutes(notifier, jwtSecret) {
12315
12422
  const orgId = payload.organizationId;
12316
12423
  if (typeof orgId !== "string" || orgId.length === 0) return;
12317
12424
  const isPulseTick = payload.type === "pulse:tick" && typeof payload.agents === "object" && payload.agents !== null;
12425
+ const isNotification = payload.type === "notification";
12426
+ const notificationAgentId = isNotification && typeof payload.agentId === "string" && payload.agentId.length > 0 ? payload.agentId : null;
12318
12427
  const sharedData = isPulseTick ? null : JSON.stringify(payload);
12319
12428
  for (const [ws, meta] of adminSockets) {
12320
12429
  if (ws.readyState !== 1 || meta.organizationId !== orgId) continue;
@@ -12324,7 +12433,10 @@ function adminWsRoutes(notifier, jwtSecret) {
12324
12433
  ...payload,
12325
12434
  agents: filtered
12326
12435
  }));
12327
- } else ws.send(sharedData);
12436
+ } else {
12437
+ if (isNotification && notificationAgentId && !meta.visibleAgentIds.has(notificationAgentId)) continue;
12438
+ ws.send(sharedData);
12439
+ }
12328
12440
  }
12329
12441
  }
12330
12442
  registerAdminBroadcaster(broadcastOrgScoped);
@@ -13245,8 +13357,8 @@ function clientWsRoutes(notifier, instanceId) {
13245
13357
  organizationId: session.organizationId,
13246
13358
  notifier
13247
13359
  });
13248
- if (payload.runtimeState === "error" && shouldNotify(agentId, "agent_error")) notifyAgentEvent(app.db, agentId, "agent_error", "high", `Agent ${agentId} entered error state`).catch(() => {});
13249
- else if (payload.runtimeState === "blocked" && shouldNotify(agentId, "agent_blocked")) notifyAgentEvent(app.db, agentId, "agent_blocked", "medium", `Agent ${agentId} is blocked`).catch(() => {});
13360
+ if (payload.runtimeState === "error" && shouldNotify(agentId, "agent_error")) notifyAgentEvent(app.db, agentId, "agent_error", "high").catch(() => {});
13361
+ else if (payload.runtimeState === "blocked" && shouldNotify(agentId, "agent_blocked")) notifyAgentEvent(app.db, agentId, "agent_blocked", "medium").catch(() => {});
13250
13362
  } else if (type === "session:event") {
13251
13363
  const agentId = parsed.data.agentId;
13252
13364
  if (!agentId || !boundAgents.has(agentId)) {
@@ -13277,7 +13389,7 @@ function clientWsRoutes(notifier, instanceId) {
13277
13389
  return;
13278
13390
  }
13279
13391
  const payload = sessionCompletionMessageSchema.parse(msg);
13280
- if (shouldNotify(agentId, `session_completed:${payload.chatId}`)) notifyAgentEvent(app.db, agentId, "session_completed", "low", `Agent ${agentId} completed a task`, payload.chatId).catch(() => {});
13392
+ if (shouldNotify(agentId, `session_completed:${payload.chatId}`)) notifyAgentEvent(app.db, agentId, "session_completed", "low", payload.chatId).catch(() => {});
13281
13393
  } else if (type === "heartbeat") {
13282
13394
  if (clientId) {
13283
13395
  await heartbeatClient(app.db, clientId);
@@ -13302,7 +13414,7 @@ function clientWsRoutes(notifier, instanceId) {
13302
13414
  notifier.unsubscribe(info.inboxId, socket);
13303
13415
  if (getAgentClientId(agentId) === clientId) try {
13304
13416
  await unbindAgent(app.db, agentId);
13305
- if (shouldNotify(agentId, "agent_disconnected")) notifyAgentEvent(app.db, agentId, "agent_disconnected", "medium", `Agent ${agentId} disconnected`).catch(() => {});
13417
+ if (shouldNotify(agentId, "agent_disconnected")) notifyAgentEvent(app.db, agentId, "agent_disconnected", "medium").catch(() => {});
13306
13418
  } catch {}
13307
13419
  }
13308
13420
  boundAgents.clear();
@@ -14804,7 +14916,7 @@ function createBackgroundTasks(app, instanceId, adapterManager, kaelRuntime) {
14804
14916
  count: staleAgents.length,
14805
14917
  agentIds: staleAgents
14806
14918
  }, "marked agents as stale");
14807
- for (const agentId of staleAgents) notifyAgentEvent(app.db, agentId, "agent_stale", "medium", `Agent ${agentId} is unresponsive`).catch(() => {});
14919
+ for (const agentId of staleAgents) notifyAgentEvent(app.db, agentId, "agent_stale", "medium").catch(() => {});
14808
14920
  }
14809
14921
  } catch (err) {
14810
14922
  log.error({ err }, "failed to heartbeat / cleanup presence");
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import "./logger-core-2yeIU1fc-B-__AsQO.mjs";
2
2
  import { a as resolveAccessToken, n as ensureFreshAccessToken, o as resolveServerUrl, r as ensureFreshAdminToken } from "./bootstrap-CWcBzk6C.mjs";
3
3
  import "./observability-CJzDFY_G-CmvgUuzc.mjs";
4
- import { A as printResults, B as SdkError, C as checkDatabase, D as checkServerHealth, E as checkServerConfig, F as stopPostgres, I as ClientRuntime, L as createOwner, M as status, N as ensurePostgres, O as checkServerReachable, P as isDockerAvailable, R as hasUser, S as checkClientConfig, T as checkNodeVersion, _ as isServiceSupported, b as runMigrations, c as promptMissingFields, d as onboardCheck, f as onboardCreate, g as installClientService, h as getClientServiceStatus, i as startServer, j as blank, k as checkWebSocket, l as formatCheckReport, m as runHomeMigration, o as isInteractive, s as promptAddAgent, v as resolveCliInvocation, w as checkDocker, x as checkAgentConfigs, y as uninstallClientService, z as FirstTreeHubSDK } from "./core-DZDhomaN.mjs";
4
+ import { A as printResults, B as SdkError, C as checkDatabase, D as checkServerHealth, E as checkServerConfig, F as stopPostgres, I as ClientRuntime, L as createOwner, M as status, N as ensurePostgres, O as checkServerReachable, P as isDockerAvailable, R as hasUser, S as checkClientConfig, T as checkNodeVersion, _ as isServiceSupported, b as runMigrations, c as promptMissingFields, d as onboardCheck, f as onboardCreate, g as installClientService, h as getClientServiceStatus, i as startServer, j as blank, k as checkWebSocket, l as formatCheckReport, m as runHomeMigration, o as isInteractive, s as promptAddAgent, v as resolveCliInvocation, w as checkDocker, x as checkAgentConfigs, y as uninstallClientService, z as FirstTreeHubSDK } from "./core-DzuW7b5v.mjs";
5
5
  import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-GlaczcVf.mjs";
6
6
  export { ClientRuntime, FirstTreeHubSDK, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, getClientServiceStatus, hasUser, installClientService, isDockerAvailable, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, runHomeMigration, runMigrations, startServer, status, stopPostgres, uninstallClientService };