@agent-team-foundation/first-tree-hub 0.12.10 → 0.13.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,12 +1,12 @@
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-CW73oEYn.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-Cya2OoHz.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 onboardingEventSchema, A as createOrgFromMeSchema, B as githubStartQuerySchema, C as contextTreeSnapshotSchema, Ct as updateClientCapabilitiesSchema, D as createChatSchema, E as createAgentSchema, Et as wsAuthFrameSchema, G as isOrgSettingNamespace, H as inboxAckFrameSchema, I as githubAppInstallationClaimBodySchema, J as joinByInvitationSchema, K as isRedactedEnvValue, L as githubAppInstallationPermissionsSchema$1, M as defaultRuntimeConfigPayload, N as delegateFeishuUserSchema, O as createMeChatSchema, P as dryRunAgentRuntimeConfigSchema, Q as notificationQuerySchema, R as githubCallbackQuerySchema, S as connectTokenExchangeSchema, St as updateChatSchema, T as createAdapterMappingSchema, Tt as updateOrganizationSchema, U as inboxDeliverFrameSchema$1, V as imageInlineContentSchema, W as inboxPollQuerySchema, X as loginSchema, Y as listMeChatsQuerySchema, Z as messageSourceSchema$1, _ as agentRuntimeConfigPayloadSchema$1, _t as stripCode, a as AGENT_TYPES, at as rebindAgentSchema, bt as updateAgentRuntimeConfigSchema, ct as safeRedirectPath, d as ORG_SETTINGS_NAMESPACES$1, dt as sendMessageSchema, et as paginationQuerySchema, f as WS_AUTH_FRAME_TIMEOUT_MS, ft as sendToAgentSchema, g as agentPinnedMessageSchema$1, gt as sessionStateMessageSchema, h as agentBindRequestSchema, ht as sessionReconcileRequestSchema, i as AGENT_STATUSES, k as createMemberSchema, l as MENTION_REGEX, m as addParticipantSchema, mt as sessionEventSchema$1, n as AGENT_NAME_REGEX$1, nt as patchOnboardingSchema, o as AGENT_VISIBILITY, ot as refreshTokenSchema, p as addMeChatParticipantsSchema, pt as sessionEventMessageSchema, q as isReservedAgentName$1, r as AGENT_SELECTOR_HEADER$1, s as CHAT_ENGAGEMENT_STATUSES, st as runtimeStateMessageSchema, t as AGENT_BIND_REJECT_REASONS, tt as patchChatEngagementSchema, u as NOTIFICATION_TYPES, ut as selfServiceFeishuBotSchema, v as agentTypeSchema$1, vt as submitQuestionAnswerSchema, w as createAdapterConfigSchema, wt as updateMemberSchema, x as clientRegisterSchema, xt as updateAgentSchema, y as chatMetadataSchema$1, yt as updateAdapterConfigSchema, z as githubDevCallbackQuerySchema } from "./dist-B1GHzMLc.mjs";
6
- import { a as ConflictError, c as UnauthorizedError, i as ClientUserMismatchError$1, l as organizations, n as BadRequestError, o as ForbiddenError, r as ClientOrgMismatchError$1, s as NotFoundError, t as AppError, u as users } from "./errors-CF5evtJt-B0NTIVPt.mjs";
5
+ import { $ as notificationQuerySchema, A as createMemberSchema, B as githubDevCallbackQuerySchema, C as connectTokenExchangeSchema, Ct as updateChatSchema, D as createAgentSchema, Dt as wsAuthFrameSchema, E as createAdapterMappingSchema, Et as updateOrganizationSchema, F as dryRunAgentRuntimeConfigSchema, G as inboxPollQuerySchema, H as imageInlineContentSchema, J as isReservedAgentName$1, K as isOrgSettingNamespace, L as githubAppInstallationClaimBodySchema, N as defaultRuntimeConfigPayload, O as createChatSchema, P as delegateFeishuUserSchema, Q as messageSourceSchema$1, R as githubAppInstallationPermissionsSchema$1, S as clientRegisterSchema, St as updateAgentSchema, T as createAdapterConfigSchema, Tt as updateMemberSchema, U as inboxAckFrameSchema, V as githubStartQuerySchema, W as inboxDeliverFrameSchema$1, X as listMeChatsQuerySchema, Y as joinByInvitationSchema, Z as loginSchema, _ as agentPinnedMessageSchema$1, _t as sessionStateMessageSchema, a as AGENT_TYPES, b as chatMetadataSchema$1, bt as updateAdapterConfigSchema, ct as runtimeStateMessageSchema, d as NOTIFICATION_TYPES, dt as selfServiceFeishuBotSchema, et as onboardingEventSchema, f as ORG_SETTINGS_NAMESPACES$1, ft as sendMessageSchema, g as agentBindRequestSchema, gt as sessionReconcileRequestSchema, h as addParticipantSchema, ht as sessionEventSchema$1, i as AGENT_STATUSES, j as createOrgFromMeSchema, k as createMeChatSchema, lt as safeRedirectPath, m as addMeChatParticipantsSchema, mt as sessionEventMessageSchema, n as AGENT_NAME_REGEX$1, nt as patchChatEngagementSchema, o as AGENT_VISIBILITY, ot as rebindAgentSchema, p as WS_AUTH_FRAME_TIMEOUT_MS, pt as sendToAgentSchema, q as isRedactedEnvValue, r as AGENT_SELECTOR_HEADER$1, rt as patchOnboardingSchema, s as CHAT_ENGAGEMENT_STATUSES, st as refreshTokenSchema, t as AGENT_BIND_REJECT_REASONS, tt as paginationQuerySchema, u as MENTION_REGEX, v as agentRuntimeConfigPayloadSchema$1, vt as stripCode, w as contextTreeSnapshotSchema, wt as updateClientCapabilitiesSchema, xt as updateAgentRuntimeConfigSchema, y as agentTypeSchema$1, yt as submitQuestionAnswerSchema, z as githubCallbackQuerySchema } from "./dist-C8yStx2L.mjs";
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-BViGcaUC-CZb2Svgh.mjs";
9
- import { a as invitationRedemptions, c as recordRedemption, i as getActiveInvitation, l as rotateInvitation, n as ensureActiveInvitation, o as invitations, r as findActiveByToken, t as buildInviteUrl, u as uuidv7 } from "./invitation-Bg0TRiyx-BsZH4GCS.mjs";
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-BH4CmUL0-CybE3kuP.mjs";
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";
12
12
  import { basename, delimiter, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
@@ -386,6 +386,7 @@ z.object({
386
386
  const PROMPT_APPEND_MAX_LENGTH = 32e3;
387
387
  const MCP_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
388
388
  const ENV_KEY_PATTERN = /^[A-Z][A-Z0-9_]*$/;
389
+ const WINDOWS_DRIVE_PATH_PATTERN = /^[A-Za-z]:/;
389
390
  const promptConfigSchema = z.object({ append: z.string().max(PROMPT_APPEND_MAX_LENGTH).default("") });
390
391
  const mcpStdioServerSchema = z.object({
391
392
  name: z.string().regex(MCP_NAME_PATTERN, "MCP name must match /^[a-z0-9][a-z0-9_-]{0,63}$/i"),
@@ -415,10 +416,38 @@ const envEntrySchema = z.object({
415
416
  value: z.string(),
416
417
  sensitive: z.boolean().default(false)
417
418
  });
419
+ function hasControlCharacters(value) {
420
+ for (let idx = 0; idx < value.length; idx++) {
421
+ const code = value.charCodeAt(idx);
422
+ if (code <= 31 || code === 127) return true;
423
+ }
424
+ return false;
425
+ }
426
+ function getRepoLocalPathSafetyError(localPath) {
427
+ if (localPath.length === 0) return "Git repo local path must not be empty";
428
+ if (localPath.trim() !== localPath) return "Git repo local path must not have leading or trailing whitespace";
429
+ if (hasControlCharacters(localPath)) return "Git repo local path must not contain control characters";
430
+ if (localPath.includes("\\")) return "Git repo local path must use forward slashes";
431
+ if (localPath.startsWith("/") || WINDOWS_DRIVE_PATH_PATTERN.test(localPath)) return "Git repo local path must be relative";
432
+ const segments = localPath.split("/");
433
+ for (const segment of segments) {
434
+ if (!segment) return "Git repo local path must not contain empty path segments";
435
+ if (segment === "." || segment === "..") return "Git repo local path must not contain dot segments";
436
+ if (segment.trim() !== segment) return "Git repo local path segments must not have leading or trailing whitespace";
437
+ }
438
+ return null;
439
+ }
418
440
  const gitRepoSchema = z.object({
419
441
  url: z.string().min(1),
420
442
  ref: z.string().min(1).optional(),
421
- localPath: z.string().min(1).optional()
443
+ localPath: z.string().min(1).superRefine((localPath, ctx) => {
444
+ const safetyError = getRepoLocalPathSafetyError(localPath);
445
+ if (!safetyError) return;
446
+ ctx.addIssue({
447
+ code: z.ZodIssueCode.custom,
448
+ message: safetyError
449
+ });
450
+ }).optional()
422
451
  });
423
452
  /**
424
453
  * Untagged base shape — 5 user-tunable fields, no `kind` discriminator.
@@ -1261,6 +1290,26 @@ const meChatParticipantSchema = z.object({
1261
1290
  displayName: z.string(),
1262
1291
  type: z.string()
1263
1292
  });
1293
+ /**
1294
+ * Live activity hint surfaced in the conversation row's time slot. Derived
1295
+ * server-side from the latest `session_events` row for the chat. See
1296
+ * `MeChatRow.liveActivity` for the lifecycle rules.
1297
+ *
1298
+ * `kind` is intentionally narrower than the full `sessionEventKind` enum:
1299
+ * `turn_end` / `error` produce `liveActivity: null` rather than a live
1300
+ * indicator.
1301
+ */
1302
+ const liveActivityKindSchema = z.enum([
1303
+ "tool_call",
1304
+ "thinking",
1305
+ "assistant_text"
1306
+ ]);
1307
+ const liveActivitySchema = z.object({
1308
+ agentId: z.string(),
1309
+ kind: liveActivityKindSchema,
1310
+ label: z.string(),
1311
+ startedAt: z.string()
1312
+ });
1264
1313
  const meChatRowSchema = z.object({
1265
1314
  chatId: z.string(),
1266
1315
  type: z.string(),
@@ -1274,7 +1323,8 @@ const meChatRowSchema = z.object({
1274
1323
  unreadMentionCount: z.number().int(),
1275
1324
  canReply: z.boolean(),
1276
1325
  engagementStatus: chatEngagementStatusSchema,
1277
- workingAgentIds: z.array(z.string())
1326
+ engagedAgentIds: z.array(z.string()),
1327
+ liveActivity: liveActivitySchema.nullable()
1278
1328
  });
1279
1329
  z.object({
1280
1330
  rows: z.array(meChatRowSchema),
@@ -3309,7 +3359,7 @@ You are running inside **Agent Hub**, a messaging platform for agent teams.
3309
3359
 
3310
3360
  - Messages from other team members arrive as your prompt input
3311
3361
  - Each message includes a \`[From: <agent-name>]\` header — that name is also
3312
- what you pass back to \`agent send\` to reply to or address that agent
3362
+ what you pass back to \`chat send\` to reply to or address that agent
3313
3363
  - **Your final text response is automatically delivered** to the chat — just respond normally
3314
3364
  - For **proactive communication** (sending to other agents, other chats, or structured data),
3315
3365
  use the \`first-tree-hub\` CLI below
@@ -3333,7 +3383,7 @@ The \`first-tree-hub\` CLI reads these automatically — no extra setup needed.
3333
3383
 
3334
3384
  ## Sending Messages
3335
3385
 
3336
- Use the \`first-tree-hub agent send\` CLI — it reads the env vars above and
3386
+ Use the \`first-tree-hub chat send\` CLI — it reads the env vars above and
3337
3387
  attaches the \`Authorization\` + \`X-Agent-Id\` headers automatically:
3338
3388
 
3339
3389
  \`\`\`bash
@@ -3345,28 +3395,38 @@ attaches the \`Authorization\` + \`X-Agent-Id\` headers automatically:
3345
3395
  # the case in a group chat where someone @-mentioned you to talk to them),
3346
3396
  # the message stays in that chat. Otherwise it falls back to a direct chat
3347
3397
  # between you and the recipient. You don't need to think about which.
3348
- first-tree-hub agent send <agentName> "your message"
3398
+ first-tree-hub chat send <agentName> "your message"
3349
3399
 
3350
3400
  # Send into a specific chat by id — use this only when you explicitly want
3351
3401
  # to address a chat your current session is NOT bound to.
3352
- first-tree-hub agent send --chat <chatId> "your message"
3402
+ first-tree-hub chat send --chat <chatId> "your message"
3353
3403
 
3354
3404
  # Send markdown (default format is text)
3355
- first-tree-hub agent send <agentName> -f markdown "**bold** message"
3405
+ first-tree-hub chat send <agentName> -f markdown "**bold** message"
3356
3406
 
3357
3407
  # Reply to a specific message
3358
- first-tree-hub agent send <agentName> --reply-to <messageId> "reply content"
3408
+ first-tree-hub chat send <agentName> --reply-to <messageId> "reply content"
3359
3409
 
3360
3410
  # Pipe long content via stdin (recommended for special characters)
3361
- echo "long message body" | first-tree-hub agent send <agentName>
3411
+ echo "long message body" | first-tree-hub chat send <agentName>
3362
3412
  \`\`\`
3363
3413
 
3364
- > Agent uuids appear in \`agent chats\`, chat history, and participant lists,
3365
- > but they are NOT accepted by \`agent send\` — always use the name.
3414
+ > Agent uuids appear in \`chat list\`, chat history, and participant lists,
3415
+ > but they are NOT accepted by \`chat send\` — always use the name.
3366
3416
 
3367
3417
  For content with quotes, \`$\`, backticks, or newlines, prefer stdin to avoid shell escaping issues.
3368
3418
  `;
3369
3419
  }
3420
+ function resolveGitRepoTargetPath(workspace, localPath) {
3421
+ const safetyError = getRepoLocalPathSafetyError(localPath);
3422
+ if (safetyError) throw new Error(`Unsafe git repo localPath "${localPath}": ${safetyError}`);
3423
+ const workspaceRoot = resolve(workspace);
3424
+ const targetPath = resolve(workspaceRoot, localPath);
3425
+ const relativeTarget = relative(workspaceRoot, targetPath);
3426
+ const escapesWorkspace = relativeTarget === ".." || relativeTarget.startsWith(`..${sep}`);
3427
+ if (!relativeTarget || escapesWorkspace || isAbsolute(relativeTarget)) throw new Error(`Unsafe git repo localPath "${localPath}": resolved path escapes the session workspace`);
3428
+ return targetPath;
3429
+ }
3370
3430
  const DEFAULT_CLONE_TIMEOUT_MS = 300 * 1e3;
3371
3431
  const FETCH_REFSPEC = "+refs/heads/*:refs/remotes/origin/*";
3372
3432
  const SESSION_BRANCH_PREFIX = "hub-session";
@@ -4994,7 +5054,7 @@ const createClaudeCodeHandler = (config) => {
4994
5054
  if (!gitMirrorManager || !payload?.gitRepos?.length) return;
4995
5055
  for (const repo of payload.gitRepos) {
4996
5056
  const localPath = repo.localPath ?? deriveRepoLocalPath(repo.url);
4997
- const targetPath = join(workspace, localPath);
5057
+ const targetPath = resolveGitRepoTargetPath(workspace, localPath);
4998
5058
  sessionCtx.log(`Git: preparing ${repo.url} → ${localPath}${repo.ref ? ` @ ${repo.ref}` : ""}`);
4999
5059
  const mirror = await gitMirrorManager.ensureMirror(repo.url);
5000
5060
  if (mirror.cloned) sessionCtx.log(`Git: cloned ${repo.url} in ${mirror.elapsedMs}ms`);
@@ -5205,7 +5265,7 @@ function buildCodexThreadOptions(payload, workspaceCwd) {
5205
5265
  for (const repo of payload.gitRepos) {
5206
5266
  const localPath = repo.localPath ?? deriveRepoLocalPath(repo.url);
5207
5267
  if (!localPath) continue;
5208
- additionalDirectories.push(join(workspaceCwd, localPath));
5268
+ additionalDirectories.push(resolveGitRepoTargetPath(workspaceCwd, localPath));
5209
5269
  }
5210
5270
  const opts = {
5211
5271
  workingDirectory: workspaceCwd,
@@ -5302,7 +5362,7 @@ const createCodexHandler = (config) => {
5302
5362
  for (const repo of payload.gitRepos) {
5303
5363
  const localPath = repo.localPath ?? deriveRepoLocalPath(repo.url);
5304
5364
  if (!localPath) continue;
5305
- const targetPath = join(workspaceCwd, localPath);
5365
+ const targetPath = resolveGitRepoTargetPath(workspaceCwd, localPath);
5306
5366
  if (existsSync(targetPath)) continue;
5307
5367
  try {
5308
5368
  await gitMirrorManager.ensureMirror(repo.url);
@@ -7276,7 +7336,7 @@ function fail(code, message, exitCode = 1) {
7276
7336
  //#endregion
7277
7337
  //#region src/core/agent-messaging.ts
7278
7338
  /**
7279
- * Resolve `replyTo` envelope fields for `agent send`. When the CLI is invoked
7339
+ * Resolve `replyTo` envelope fields for `chat send`. When the CLI is invoked
7280
7340
  * from inside a claude-code session (the handler exports
7281
7341
  * `FIRST_TREE_HUB_CHAT_ID` + `FIRST_TREE_HUB_INBOX_ID`), default the reply
7282
7342
  * target to the calling session's own chat so the peer's reply routes back
@@ -7568,7 +7628,7 @@ function rotateClientIdWithBackup(configDir) {
7568
7628
  }
7569
7629
  /**
7570
7630
  * Shared handler for `CLIENT_ORG_MISMATCH` across CLI entry points
7571
- * (`client start` and `client connect --no-service`). Prompts interactively,
7631
+ * (`client start` and `connect <token> --no-service`). Prompts interactively,
7572
7632
  * rotates the local clientId, and always exits the current process — the
7573
7633
  * runtime is already poisoned (wrong clientId in memory), so continuing
7574
7634
  * in-band is not safe. Service-supervised (managed) runs skip the prompt and
@@ -8331,7 +8391,7 @@ function installLaunchd() {
8331
8391
  lastBootstrapErr = res;
8332
8392
  if (attempt < 2) sleepSync(1e3);
8333
8393
  }
8334
- if (lastBootstrapErr) throw new Error(`launchctl bootstrap failed: ${lastBootstrapErr.stderr || `exit ${lastBootstrapErr.code ?? "unknown"}`}\n Command: launchctl bootstrap ${target} ${plistPath}\n Recovery: \`launchctl bootout ${target}/${LAUNCHD_LABEL}\` then \`first-tree-hub client connect <server-url>\`.`);
8394
+ if (lastBootstrapErr) throw new Error(`launchctl bootstrap failed: ${lastBootstrapErr.stderr || `exit ${lastBootstrapErr.code ?? "unknown"}`}\n Command: launchctl bootstrap ${target} ${plistPath}\n Recovery: \`launchctl bootout ${target}/${LAUNCHD_LABEL}\` then \`first-tree-hub connect <token>\`.`);
8335
8395
  const enableRes = runCapture("launchctl", ["enable", `${target}/${LAUNCHD_LABEL}`], 5e3);
8336
8396
  if (!enableRes.ok) print.line(` warning: launchctl enable: ${enableRes.stderr || `exit ${enableRes.code ?? "unknown"}`}\n`);
8337
8397
  const { state, pid, detail } = launchdState();
@@ -8491,7 +8551,7 @@ function installSystemd() {
8491
8551
  "--now",
8492
8552
  SYSTEMD_UNIT
8493
8553
  ], 1e4);
8494
- if (!enableRes.ok) throw new Error(`systemctl --user enable --now ${SYSTEMD_UNIT} failed: ${enableRes.stderr || `exit ${enableRes.code ?? "unknown"}`}\n Recovery: \`systemctl --user stop ${SYSTEMD_UNIT}\` then \`first-tree-hub client connect <server-url>\`.`);
8554
+ if (!enableRes.ok) throw new Error(`systemctl --user enable --now ${SYSTEMD_UNIT} failed: ${enableRes.stderr || `exit ${enableRes.code ?? "unknown"}`}\n Recovery: \`systemctl --user stop ${SYSTEMD_UNIT}\` then \`first-tree-hub connect <token>\`.`);
8495
8555
  const { state, pid, detail } = systemdState();
8496
8556
  return {
8497
8557
  platform: "systemd",
@@ -9003,7 +9063,7 @@ function checkBackgroundService() {
9003
9063
  return {
9004
9064
  label: "Background service",
9005
9065
  ok: false,
9006
- detail: "not installed — re-run `first-tree-hub client connect <url>` to install"
9066
+ detail: "not installed — re-run `first-tree-hub connect <token>` to install"
9007
9067
  };
9008
9068
  }
9009
9069
  async function checkWebSocket() {
@@ -9297,7 +9357,7 @@ function runHomeMigration() {
9297
9357
  }
9298
9358
  print.line(`[first-tree-hub] Copied client home to new layout: ${result.from} → ${result.to}\n (Legacy directory preserved as a backup — delete it manually once you've verified the new location works.)\n`);
9299
9359
  if (process.argv.includes("--no-interactive")) {
9300
- print.line("[first-tree-hub] Note: running as background service — skipped auto re-register to avoid self-termination.\n Service paths will refresh on the next `first-tree-hub client connect <url>`.\n");
9360
+ print.line("[first-tree-hub] Note: running as background service — skipped auto re-register to avoid self-termination.\n Service paths will refresh on the next `first-tree-hub connect <token>`.\n");
9301
9361
  return;
9302
9362
  }
9303
9363
  const status = getClientServiceStatus();
@@ -9307,7 +9367,7 @@ function runHomeMigration() {
9307
9367
  print.line(`[first-tree-hub] Re-registered background service with new home paths.\n`);
9308
9368
  } catch (err) {
9309
9369
  const msg = err instanceof Error ? err.message : String(err);
9310
- print.line(`[first-tree-hub] WARNING: home migration succeeded but re-registering the background service failed: ${msg}\n Re-run \`first-tree-hub client connect <url>\` to refresh service paths.\n`);
9370
+ print.line(`[first-tree-hub] WARNING: home migration succeeded but re-registering the background service failed: ${msg}\n Re-run \`first-tree-hub connect <token>\` to refresh service paths.\n`);
9311
9371
  }
9312
9372
  }
9313
9373
  //#endregion
@@ -9339,7 +9399,7 @@ async function onboardCheck(args) {
9339
9399
  key: "connect",
9340
9400
  label: "Signed in",
9341
9401
  status: "missing_required",
9342
- hint: "Run `first-tree-hub client connect <server-url>` first"
9402
+ hint: "Run `first-tree-hub connect <token>` first"
9343
9403
  });
9344
9404
  try {
9345
9405
  const serverUrl = resolveServerUrl(args.server);
@@ -9493,7 +9553,7 @@ async function onboardCreate(args) {
9493
9553
  }
9494
9554
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
9495
9555
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
9496
- const { bindFeishuBot } = await import("./feishu-30vUx69l.mjs").then((n) => n.r);
9556
+ const { bindFeishuBot } = await import("./feishu-D_vnqC6a.mjs").then((n) => n.r);
9497
9557
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
9498
9558
  if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
9499
9559
  else {
@@ -9580,13 +9640,13 @@ async function promptMissingFields(options) {
9580
9640
  * add (there's nothing sensible to key the local dir on).
9581
9641
  */
9582
9642
  async function promptAddAgent(opts = {}) {
9583
- if (loadCredentials() === null) throw new Error("Not connected. Run `first-tree-hub client connect <server-url>` first.");
9643
+ if (loadCredentials() === null) throw new Error("Not connected. Run `first-tree-hub connect <token>` first.");
9584
9644
  let serverUrl;
9585
9645
  try {
9586
9646
  serverUrl = resolveServerUrl(process.env.FIRST_TREE_HUB_SERVER_URL);
9587
9647
  } catch (err) {
9588
9648
  const msg = err instanceof Error ? err.message : String(err);
9589
- throw new Error(`${msg} Run \`first-tree-hub client connect\` or set FIRST_TREE_HUB_SERVER_URL.`);
9649
+ throw new Error(`${msg} Run \`first-tree-hub connect <token>\` or set FIRST_TREE_HUB_SERVER_URL.`);
9590
9650
  }
9591
9651
  const agentId = opts.agentId ?? await input({
9592
9652
  message: "Agent UUID on the Hub:",
@@ -10706,7 +10766,7 @@ function createFeedbackHandler(config) {
10706
10766
  return { handle };
10707
10767
  }
10708
10768
  //#endregion
10709
- //#region ../server/dist/app-B1wsm8zG.mjs
10769
+ //#region ../server/dist/app-DQVUb4ZY.mjs
10710
10770
  var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
10711
10771
  init_esm();
10712
10772
  var __defProp = Object.defineProperty;
@@ -11406,7 +11466,7 @@ async function resolveAgentClient(db, data) {
11406
11466
  userId: clients.userId
11407
11467
  }).from(clients).where(eq(clients.id, data.clientId)).limit(1);
11408
11468
  if (!client) throw new BadRequestError(`Client "${data.clientId}" not found`);
11409
- if (!client.userId) throw new BadRequestError(`Client "${data.clientId}" has not been claimed by a user yet. Have the operator run \`first-tree-hub client connect\` on that machine before pinning an agent to it.`);
11469
+ if (!client.userId) throw new BadRequestError(`Client "${data.clientId}" has not been claimed by a user yet. Have the operator run \`first-tree-hub connect <token>\` on that machine before pinning an agent to it.`);
11410
11470
  if (client.userId !== manager.userId) throw new ForbiddenError(`Client "${data.clientId}" is not owned by the manager's user — pick a client belonging to that user.`);
11411
11471
  return client.id;
11412
11472
  }
@@ -12964,8 +13024,9 @@ async function pushToWebhook(notification) {
12964
13024
  }
12965
13025
  /**
12966
13026
  * Session events — structured event stream per (agent, chat) session.
12967
- * `kind` is 'tool_call' | 'error'; the payload shape is enforced by the
12968
- * service layer via Zod (no FK / CHECK on this table per project rule).
13027
+ * `kind` is one of `'tool_call' | 'error' | 'assistant_text' | 'thinking'
13028
+ * | 'turn_end'`; payload shape per kind is enforced by the service layer
13029
+ * via Zod (no FK / CHECK on this table per project rule).
12969
13030
  *
12970
13031
  * `seq` is monotonic per (agent_id, chat_id). The single-writer invariant
12971
13032
  * in the client-side session-manager guarantees ordering; the service wraps
@@ -13623,9 +13684,11 @@ function clientWsRoutes(notifier, instanceId) {
13623
13684
  return;
13624
13685
  }
13625
13686
  const payload = sessionEventMessageSchema.parse(msg);
13687
+ const boundInfo = boundAgents.get(agentId);
13626
13688
  chainSessionOp(agentId, payload.chatId, async () => {
13627
13689
  try {
13628
13690
  await appendEvent(app.db, agentId, payload.chatId, payload.event);
13691
+ if (boundInfo) notifier.notifySessionEvent(agentId, payload.chatId, payload.event.kind, boundInfo.organizationId).catch(() => {});
13629
13692
  } catch (err) {
13630
13693
  socket.send(JSON.stringify({
13631
13694
  type: "error",
@@ -13827,7 +13890,7 @@ async function agentRoutes(app) {
13827
13890
  };
13828
13891
  if (health === "disconnected") return reply.status(200).send({
13829
13892
  status: "offline",
13830
- message: "Agent is not connected. Start the client with: first-tree-hub client connect <server-url>",
13893
+ message: "Agent is not connected. Connect the client with: first-tree-hub connect <token>",
13831
13894
  connection
13832
13895
  });
13833
13896
  if (health === "stale") return reply.status(200).send({
@@ -15857,10 +15920,10 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
15857
15920
  agentId: chatMembership.agentId,
15858
15921
  displayName: agents.displayName,
15859
15922
  type: agents.type,
15860
- runtimeState: agentPresence.runtimeState
15861
- }).from(chatMembership).innerJoin(agents, eq(chatMembership.agentId, agents.uuid)).leftJoin(agentPresence, eq(agentPresence.agentId, chatMembership.agentId)).where(and(inArray(chatMembership.chatId, chatIds), eq(chatMembership.accessMode, "speaker")));
15923
+ sessionState: agentChatSessions.state
15924
+ }).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")));
15862
15925
  const participantsByChat = /* @__PURE__ */ new Map();
15863
- const workingByChat = /* @__PURE__ */ new Map();
15926
+ const engagedByChat = /* @__PURE__ */ new Map();
15864
15927
  for (const p of participantRows) {
15865
15928
  const list = participantsByChat.get(p.chatId) ?? [];
15866
15929
  list.push({
@@ -15869,12 +15932,13 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
15869
15932
  type: p.type
15870
15933
  });
15871
15934
  participantsByChat.set(p.chatId, list);
15872
- if (p.runtimeState === "working") {
15873
- const working = workingByChat.get(p.chatId) ?? [];
15874
- working.push(p.agentId);
15875
- workingByChat.set(p.chatId, working);
15935
+ if (p.sessionState === "active") {
15936
+ const engaged = engagedByChat.get(p.chatId) ?? [];
15937
+ engaged.push(p.agentId);
15938
+ engagedByChat.set(p.chatId, engaged);
15876
15939
  }
15877
15940
  }
15941
+ const liveActivityByChat = await deriveLiveActivity(db, chatIds);
15878
15942
  const firstMessageRows = chatIds.length > 0 ? await db.selectDistinctOn([messages.chatId], {
15879
15943
  chatId: messages.chatId,
15880
15944
  content: messages.content
@@ -15902,13 +15966,98 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
15902
15966
  unreadMentionCount: r.unread_mention_count,
15903
15967
  canReply: isSpeaker,
15904
15968
  engagementStatus: r.engagement_status,
15905
- workingAgentIds: workingByChat.get(r.chat_id) ?? []
15969
+ engagedAgentIds: engagedByChat.get(r.chat_id) ?? [],
15970
+ liveActivity: liveActivityByChat.get(r.chat_id) ?? null
15906
15971
  };
15907
15972
  }),
15908
15973
  nextCursor
15909
15974
  };
15910
15975
  }
15911
15976
  /**
15977
+ * Per-chat live activity, derived from the most recent `session_events` row.
15978
+ *
15979
+ * Returns a chatId → LiveActivity map; chats with no activity (or where the
15980
+ * latest event is terminal / stale) are absent from the map (caller treats
15981
+ * absence as null).
15982
+ */
15983
+ async function deriveLiveActivity(db, chatIds) {
15984
+ if (chatIds.length === 0) return /* @__PURE__ */ new Map();
15985
+ const chatIdInClause = sql.join(chatIds.map((id) => sql`${id}`), sql`, `);
15986
+ const rows = (await db.execute(sql`
15987
+ SELECT acs.agent_id AS agent_id,
15988
+ acs.chat_id AS chat_id,
15989
+ e.kind AS kind,
15990
+ e.payload AS payload,
15991
+ e.created_at AS created_at
15992
+ FROM agent_chat_sessions acs
15993
+ CROSS JOIN LATERAL (
15994
+ SELECT kind, payload, created_at, seq
15995
+ FROM session_events se
15996
+ WHERE se.agent_id = acs.agent_id
15997
+ AND se.chat_id = acs.chat_id
15998
+ ORDER BY se.seq DESC
15999
+ LIMIT 1
16000
+ ) e
16001
+ WHERE acs.chat_id IN (${chatIdInClause})
16002
+ AND acs.state <> 'evicted'
16003
+ `)).map((r) => ({
16004
+ agent_id: r.agent_id,
16005
+ chat_id: r.chat_id,
16006
+ kind: r.kind,
16007
+ payload: r.payload,
16008
+ created_at: r.created_at
16009
+ }));
16010
+ const now = Date.now();
16011
+ const byChat = /* @__PURE__ */ new Map();
16012
+ for (const row of rows) {
16013
+ const activity = toLiveActivity(row);
16014
+ if (!activity) continue;
16015
+ const createdAtMs = new Date(row.created_at).getTime();
16016
+ if (now - createdAtMs > 6e4) continue;
16017
+ const existing = byChat.get(row.chat_id);
16018
+ if (!existing || createdAtMs > existing.createdAtMs) byChat.set(row.chat_id, {
16019
+ activity,
16020
+ createdAtMs
16021
+ });
16022
+ }
16023
+ const out = /* @__PURE__ */ new Map();
16024
+ for (const [chatId, { activity }] of byChat) out.set(chatId, activity);
16025
+ return out;
16026
+ }
16027
+ /**
16028
+ * Translate a `session_events` row into a `LiveActivity`, or null when the
16029
+ * kind is terminal (`turn_end` / `error`) or unrecognised. Pure & exported
16030
+ * for unit testing.
16031
+ */
16032
+ function toLiveActivity(row) {
16033
+ const startedAt = new Date(row.created_at).toISOString();
16034
+ switch (row.kind) {
16035
+ case "tool_call": {
16036
+ const payload = row.payload ?? {};
16037
+ const label = typeof payload.name === "string" && payload.name.length > 0 ? payload.name : "Tool";
16038
+ return {
16039
+ agentId: row.agent_id,
16040
+ kind: "tool_call",
16041
+ label,
16042
+ startedAt
16043
+ };
16044
+ }
16045
+ case "thinking": return {
16046
+ agentId: row.agent_id,
16047
+ kind: "thinking",
16048
+ label: "Thinking",
16049
+ startedAt
16050
+ };
16051
+ case "assistant_text": return {
16052
+ agentId: row.agent_id,
16053
+ kind: "assistant_text",
16054
+ label: "Writing",
16055
+ startedAt
16056
+ };
16057
+ default: return null;
16058
+ }
16059
+ }
16060
+ /**
15912
16061
  * Title resolution priority:
15913
16062
  *
15914
16063
  * 1. `chat.topic` (manual, set via `PATCH /chats/:chatId`)
@@ -17570,7 +17719,7 @@ async function healthzRoutes(app) {
17570
17719
  * `api/orgs/invitations.ts` (Class B, admin-gated).
17571
17720
  */
17572
17721
  async function publicInvitationRoutes(app) {
17573
- const { previewInvitation } = await import("./invitation-C299fxkP-CZgGbsN_.mjs");
17722
+ const { previewInvitation } = await import("./invitation-CNv7gfFF-D93KQte0.mjs");
17574
17723
  app.get("/:token/preview", async (request, reply) => {
17575
17724
  if (!request.params.token) throw new UnauthorizedError("Token required");
17576
17725
  const preview = await previewInvitation(app.db, request.params.token);
@@ -17640,7 +17789,8 @@ async function meRoutes(app) {
17640
17789
  username: users.username,
17641
17790
  displayName: users.displayName,
17642
17791
  avatarUrl: users.avatarUrl,
17643
- onboardingDismissedAt: users.onboardingDismissedAt
17792
+ onboardingDismissedAt: users.onboardingDismissedAt,
17793
+ onboardingCompletedAt: users.onboardingCompletedAt
17644
17794
  }).from(users).where(eq(users.id, userId)).limit(1);
17645
17795
  const memberships = await listActiveMemberships(app.db, userId);
17646
17796
  const defaultMembership = pickDefaultMembership(memberships.map((m) => ({
@@ -17668,7 +17818,8 @@ async function meRoutes(app) {
17668
17818
  })),
17669
17819
  onboarding: {
17670
17820
  step: onboardingStep,
17671
- dismissedAt: user?.onboardingDismissedAt ? user.onboardingDismissedAt.toISOString() : null
17821
+ dismissedAt: user?.onboardingDismissedAt ? user.onboardingDismissedAt.toISOString() : null,
17822
+ completedAt: user?.onboardingCompletedAt ? user.onboardingCompletedAt.toISOString() : null
17672
17823
  },
17673
17824
  inviteUrl
17674
17825
  };
@@ -17694,6 +17845,25 @@ async function meRoutes(app) {
17694
17845
  return reply.status(200).send({ dismissedAt: u?.onboardingDismissedAt ? u.onboardingDismissedAt.toISOString() : null });
17695
17846
  });
17696
17847
  /**
17848
+ * POST /me/onboarding-completed — stamp the terminal-state column when
17849
+ * the user walks Step 3 to success (admin Continue, invitee Confirm /
17850
+ * Continue). Distinct from PATCH /me/onboarding { dismissed: true },
17851
+ * which only hides the stepper UI. Once stamped, the web sidebar drops
17852
+ * the Settings → Onboarding entry point and /settings/onboarding
17853
+ * redirects, so the wizard cannot re-enter.
17854
+ *
17855
+ * Idempotent: only writes when the column is still NULL — re-calling on
17856
+ * an already-completed user is a no-op rather than resetting the stamp.
17857
+ */
17858
+ app.post("/me/onboarding-completed", async (request, reply) => {
17859
+ const { userId } = requireUser(request);
17860
+ if ((await app.db.update(users).set({ onboardingCompletedAt: /* @__PURE__ */ new Date() }).where(and(eq(users.id, userId), isNull(users.onboardingCompletedAt))).returning({ id: users.id })).length > 0) app.log.info({
17861
+ event: "onboarding.completed",
17862
+ userId
17863
+ }, "onboarding funnel: setup completed");
17864
+ return reply.status(200).send({ ok: true });
17865
+ });
17866
+ /**
17697
17867
  * POST /me/onboarding/events — web-side onboarding funnel reporter.
17698
17868
  * Server-side milestones (`team_created` at OAuth, `dismissed` on PATCH)
17699
17869
  * are emitted directly; this endpoint surfaces the web-driven ones into
@@ -17836,7 +18006,7 @@ async function meRoutes(app) {
17836
18006
  */
17837
18007
  app.get("/me/pinned-agents", async (request) => {
17838
18008
  const { userId } = requireUser(request);
17839
- const { listMyPinnedAgents } = await import("./client-DNiLcPEq-db3YS57z.mjs");
18009
+ const { listMyPinnedAgents } = await import("./client-h4KZ3b9o-CQyibXig.mjs");
17840
18010
  return listMyPinnedAgents(app.db, { userId });
17841
18011
  });
17842
18012
  /**
@@ -18757,6 +18927,12 @@ function orgWsRoutes(notifier, jwtSecret) {
18757
18927
  ...payload
18758
18928
  });
18759
18929
  });
18930
+ notifier.onSessionEvent((payload) => {
18931
+ broadcastOrgScoped({
18932
+ type: "session:event",
18933
+ ...payload
18934
+ });
18935
+ });
18760
18936
  notifier.onChatMessage(({ chatId }) => {
18761
18937
  dispatchChatMessage(chatId);
18762
18938
  });
@@ -22147,7 +22323,7 @@ const declineUpdate = async () => false;
22147
22323
  * relaunch picks up the new binary.
22148
22324
  *
22149
22325
  * `managed=false` means the process is running standalone (e.g. manual
22150
- * `client start`, `client connect --no-service`, CI without a supervisor).
22326
+ * `client start`, `connect <token> --no-service`, CI without a supervisor).
22151
22327
  * Exiting in that mode would leave the client offline until an operator
22152
22328
  * noticed — so the callback instead prints a restart hint, returns
22153
22329
  * `{ installed: true }`, and the UpdateManager stops retrying until the
@@ -1,4 +1,4 @@
1
- import{c as r,j as e,U as o,L as s,A as i,F as a,r as l}from"./index-DiDfVdIH.js";/**
1
+ import{c as r,j as e,U as o,L as s,A as i,F as a,r as l}from"./index-DL_9NFkt.js";/**
2
2
  * @license lucide-react v0.577.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.