@agent-team-foundation/first-tree-hub 0.11.1 → 0.11.3

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.
Files changed (42) hide show
  1. package/dist/{bootstrap-TJRy0B9m.mjs → bootstrap-D4rdqM2F.mjs} +5 -2
  2. package/dist/cli/index.mjs +44 -41
  3. package/dist/cli-fetch--tiwKm5S.mjs +167 -0
  4. package/dist/client-By1K4VVT-C5K7WZo6.mjs +4 -0
  5. package/dist/{client-BCaK653p-CZjDNcdM.mjs → client-CLdRbuml-BRtalKpQ.mjs} +12 -4
  6. package/dist/{dist-BkvrONSQ.mjs → dist-BAqGZkco.mjs} +99 -1
  7. package/dist/{dist-BLY7Bu-l.mjs → dist-BQtAQNRD.mjs} +1 -1
  8. package/dist/{feishu-AEMHwT6L.mjs → feishu-Th_-ivJ7.mjs} +4 -3
  9. package/dist/{getMachineId-bsd-DjLgZlll.mjs → getMachineId-bsd-DyySs8xz.mjs} +2 -2
  10. package/dist/{getMachineId-bsd-DR4-Dysy.mjs → getMachineId-bsd-c2VImogj.mjs} +2 -2
  11. package/dist/{getMachineId-darwin-CaD2juTg.mjs → getMachineId-darwin-Cl7TSzgO.mjs} +2 -2
  12. package/dist/{getMachineId-darwin-B6WCAhc4.mjs → getMachineId-darwin-DKgI8b1d.mjs} +2 -2
  13. package/dist/{getMachineId-linux-Dk3gWdQK.mjs → getMachineId-linux-1OIMWfdh.mjs} +1 -1
  14. package/dist/{getMachineId-linux-BeWHG1gK.mjs → getMachineId-linux-cT7EbP10.mjs} +1 -1
  15. package/dist/{getMachineId-unsupported-BMJQItvF.mjs → getMachineId-unsupported-CkX-YOG1.mjs} +1 -1
  16. package/dist/{getMachineId-unsupported-Bgz_Je1J.mjs → getMachineId-unsupported-CmVlhzIo.mjs} +1 -1
  17. package/dist/{getMachineId-win-vJ6VfDRI.mjs → getMachineId-win-C2cM60YT.mjs} +2 -2
  18. package/dist/{getMachineId-win-CdgcrzCW.mjs → getMachineId-win-Chl03TYe.mjs} +2 -2
  19. package/dist/index.mjs +10 -9
  20. package/dist/invitation-DWlyNb8x-D3zjZSwI.mjs +4 -0
  21. package/dist/{invitation-Dnn5gGGX-Ce7zbZpn.mjs → invitation-Dnn5gGGX-DXryyvRG.mjs} +1 -1
  22. package/dist/{multipart-parser-BIksYTkk.mjs → multipart-parser-QRu3OKK4.mjs} +1 -1
  23. package/dist/{observability-C3nY6Jcz-Dpsi3eFk.mjs → observability-BAScT_5S-gw1ODB_o.mjs} +140 -17
  24. package/dist/observability-CYsdAcoF.mjs +5 -0
  25. package/dist/{saas-connect-Bd0g0v_b.mjs → saas-connect-gcT6Q10z.mjs} +919 -168
  26. package/dist/{src-uVZSbShB.mjs → src-DFlbpJfU.mjs} +3 -3
  27. package/dist/web/assets/index-43trJLR8.js +388 -0
  28. package/dist/web/assets/{index-Dbwa40_B.js → index-CD7rTdqm.js} +1 -1
  29. package/dist/web/assets/index-fNb_M0nL.css +1 -0
  30. package/dist/web/index.html +2 -2
  31. package/package.json +1 -1
  32. package/dist/client-m1OM4Iag-HKWgB3Yk.mjs +0 -4
  33. package/dist/invitation-DWlyNb8x-DZTW9I26.mjs +0 -4
  34. package/dist/observability-Co8OO0og.mjs +0 -5
  35. package/dist/web/assets/index-7RvlJjJ9.css +0 -1
  36. package/dist/web/assets/index-cpdSFHAJ.js +0 -383
  37. /package/dist/{errors-BmyRwN0Y-CIZZ_sDc.mjs → errors-BmyRwN0Y-Dad3eV8F.mjs} +0 -0
  38. /package/dist/{esm-iadMkGbV.mjs → esm-Ci8E1Gtj.mjs} +0 -0
  39. /package/dist/{execAsync-pImxPKN5.mjs → execAsync-DUfRkc4a.mjs} +0 -0
  40. /package/dist/{execAsync-CCyouKZM.mjs → execAsync-YbEZSOYd.mjs} +0 -0
  41. /package/dist/{from-CaD373S1.mjs → from-DQ7eNRwu.mjs} +0 -0
  42. /package/dist/{src-DNBS5Yjj.mjs → src-aJMV60mR.mjs} +0 -0
@@ -1,24 +1,25 @@
1
1
  import { a as __toCommonJS, o as __toESM, t as __commonJSMin } from "./chunk-BSw8zbkd.mjs";
2
- import { C as withSpan, D as require_pino, E as redactUrl, S as startWsConnectionSpan, T as FIRST_TREE_HUB_ATTR, a as createLogger$1, b as stampOrgScope, d as observabilityPlugin, g as setWsConnectionAttrs, i as bodyCaptureOnSendHook, l as messageAttrs, m as rootLogger$1, n as applyLoggerConfig, o as currentTraceId, p as reportErrorToRoot, r as attachRequestContext, s as endWsConnectionSpan, t as adapterAttrs, v as stampAgentResource, w as withWsMessageSpan, y as stampChatResource } from "./observability-C3nY6Jcz-Dpsi3eFk.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-TJRy0B9m.mjs";
4
- import { $ as paginationQuerySchema, A as createMeChatSchema, B as githubStartQuerySchema, Ct as wsAuthFrameSchema, D as createAdapterMappingSchema, E as createAdapterConfigSchema, F as delegateFeishuUserSchema, G as isRedactedEnvValue, H as inboxAckFrameSchema, I as dryRunAgentRuntimeConfigSchema, J as linkTaskChatSchema, K as isReservedAgentName$1, L as extractMentions, M as createOrgFromMeSchema, N as createTaskSchema, O as createAgentSchema, P as defaultRuntimeConfigPayload, Q as notificationQuerySchema, R as githubCallbackQuerySchema, S as agentTypeSchema$1, St as updateTaskStatusSchema, T as connectTokenExchangeSchema, U as inboxDeliverFrameSchema$1, V as imageInlineContentSchema, W as inboxPollQuerySchema, X as loginSchema, Y as listMeChatsQuerySchema, Z as messageSourceSchema$1, _ as adminCreateTaskSchema, _t as updateAgentSchema, a as AGENT_STATUSES, at as selfServiceFeishuBotSchema, b as agentPinnedMessageSchema$1, bt as updateMemberSchema, ct as sessionCompletionMessageSchema, d as TASK_HEALTH_SIGNALS, dt as sessionReconcileRequestSchema, et as rebindAgentSchema, f as TASK_STATUSES, ft as sessionStateMessageSchema, g as addParticipantSchema, gt as updateAgentRuntimeConfigSchema, h as addMeChatParticipantsSchema, ht as updateAdapterConfigSchema, i as AGENT_SOURCES, it as scanMentionTokens, j as createMemberSchema, k as createChatSchema, l as MENTION_REGEX, lt as sessionEventMessageSchema, m as WS_AUTH_FRAME_TIMEOUT_MS, mt as taskListQuerySchema, n as AGENT_NAME_REGEX$1, nt as runtimeStateMessageSchema, o as AGENT_TYPES, ot as sendMessageSchema, p as TASK_TERMINAL_STATUSES, pt as stripCode, q as joinByInvitationSchema, r as AGENT_SELECTOR_HEADER$1, rt as safeRedirectPath, s as AGENT_VISIBILITY, st as sendToAgentSchema, t as AGENT_BIND_REJECT_REASONS, tt as refreshTokenSchema, u as TASK_CREATOR_TYPES, ut as sessionEventSchema$1, v as adminUpdateTaskSchema, vt as updateChatSchema, w as clientRegisterSchema, x as agentRuntimeConfigPayloadSchema$1, xt as updateOrganizationSchema, y as agentBindRequestSchema, yt as updateClientCapabilitiesSchema, z as githubDevCallbackQuerySchema } from "./dist-BkvrONSQ.mjs";
5
- 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-BmyRwN0Y-CIZZ_sDc.mjs";
6
- import { C as serverInstances, D as unbindAgent, E as touchAgent, O as updateClientCapabilities, S as retireClient, T as setRuntimeState, _ as listClientsForOrgAdmin, a as claimClient, b as members, c as clients, d as getClient, f as getOnlineCount, g as listActiveAgentsPinnedToClient, h as heartbeatInstance, i as bindAgent, l as deriveAuthState, m as heartbeatClient, n as agents, o as cleanupStaleClients, p as getPresence, r as assertClientOwner, s as cleanupStalePresence, t as agentPresence, u as disconnectClient, w as setOffline, x as registerClient, y as markStaleAgents } from "./client-BCaK653p-CZjDNcdM.mjs";
7
- import { n as init_esm, r as trace, t as esm_exports } from "./esm-iadMkGbV.mjs";
8
- 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-Dnn5gGGX-Ce7zbZpn.mjs";
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, f as messageAttrs, 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-gw1ODB_o.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-D4rdqM2F.mjs";
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 notificationQuerySchema, A as createChatSchema, B as githubDevCallbackQuerySchema, Ct as updateTaskStatusSchema, D as createAdapterConfigSchema, E as contextTreeSnapshotSchema, F as defaultRuntimeConfigPayload, G as inboxPollQuerySchema, H as imageInlineContentSchema, I as delegateFeishuUserSchema, J as joinByInvitationSchema, K as isRedactedEnvValue, L as dryRunAgentRuntimeConfigSchema, M as createMemberSchema, N as createOrgFromMeSchema, O as createAdapterMappingSchema, P as createTaskSchema, Q as messageSourceSchema$1, R as extractMentions, S as agentTypeSchema$1, St as updateOrganizationSchema, T as connectTokenExchangeSchema, U as inboxAckFrameSchema, V as githubStartQuerySchema, W as inboxDeliverFrameSchema$1, X as listMeChatsQuerySchema, Y as linkTaskChatSchema, Z as loginSchema, _ as adminCreateTaskSchema, _t as updateAgentRuntimeConfigSchema, a as AGENT_STATUSES, at as scanMentionTokens, b as agentPinnedMessageSchema$1, bt as updateClientCapabilitiesSchema, ct as sendToAgentSchema, d as TASK_HEALTH_SIGNALS, dt as sessionEventSchema$1, et as paginationQuerySchema, f as TASK_STATUSES, ft as sessionReconcileRequestSchema, g as addParticipantSchema, gt as updateAdapterConfigSchema, h as addMeChatParticipantsSchema, ht as taskListQuerySchema, i as AGENT_SOURCES, it as safeRedirectPath, j as createMeChatSchema, k as createAgentSchema, l as MENTION_REGEX, lt as sessionCompletionMessageSchema, m as WS_AUTH_FRAME_TIMEOUT_MS, mt as stripCode, n as AGENT_NAME_REGEX$1, nt as refreshTokenSchema, o as AGENT_TYPES, ot as selfServiceFeishuBotSchema, p as TASK_TERMINAL_STATUSES, pt as sessionStateMessageSchema, q as isReservedAgentName$1, r as AGENT_SELECTOR_HEADER$1, rt as runtimeStateMessageSchema, s as AGENT_VISIBILITY, st as sendMessageSchema, t as AGENT_BIND_REJECT_REASONS, tt as rebindAgentSchema, u as TASK_CREATOR_TYPES, ut as sessionEventMessageSchema, v as adminUpdateTaskSchema, vt as updateAgentSchema, w as clientRegisterSchema, wt as wsAuthFrameSchema, x as agentRuntimeConfigPayloadSchema$1, xt as updateMemberSchema, y as agentBindRequestSchema, yt as updateChatSchema, z as githubCallbackQuerySchema } from "./dist-BAqGZkco.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-BmyRwN0Y-Dad3eV8F.mjs";
7
+ import { C as retireClient, D as touchAgent, E as setRuntimeState, O as unbindAgent, S as registerClient, T as setOffline, _ as listClients, a as claimClient, b as markStaleAgents, c as clients, d as getClient, f as getOnlineCount, g as listActiveAgentsPinnedToClient, h as heartbeatInstance, i as bindAgent, k as updateClientCapabilities, l as deriveAuthState, m as heartbeatClient, n as agents, o as cleanupStaleClients, p as getPresence, r as assertClientOwner, s as cleanupStalePresence, t as agentPresence, u as disconnectClient, v as listClientsForOrgAdmin, w as serverInstances, x as members } from "./client-CLdRbuml-BRtalKpQ.mjs";
8
+ import { n as init_esm, r as trace, t as esm_exports } from "./esm-Ci8E1Gtj.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-Dnn5gGGX-DXryyvRG.mjs";
9
10
  import { createRequire } from "node:module";
10
11
  import { ZodError, z } from "zod";
11
- import { basename, delimiter, dirname, isAbsolute, join, resolve } from "node:path";
12
+ import { basename, delimiter, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
12
13
  import { Writable } from "node:stream";
13
14
  import { homedir, hostname, platform, tmpdir, userInfo } from "node:os";
14
15
  import { EventEmitter } from "node:events";
15
16
  import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync, writeSync } from "node:fs";
16
17
  import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
17
18
  import WebSocket from "ws";
18
- import { mkdir, writeFile } from "node:fs/promises";
19
+ import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
19
20
  import { parse, stringify } from "yaml";
20
21
  import { query } from "@anthropic-ai/claude-agent-sdk";
21
- import { execFileSync, execSync, spawn, spawnSync } from "node:child_process";
22
+ import { execFile, execFileSync, execSync, spawn, spawnSync } from "node:child_process";
22
23
  import { Codex } from "@openai/codex-sdk";
23
24
  import { fileURLToPath } from "node:url";
24
25
  import * as semver from "semver";
@@ -29,12 +30,14 @@ import { drizzle } from "drizzle-orm/postgres-js";
29
30
  import postgres from "postgres";
30
31
  import { migrate } from "drizzle-orm/postgres-js/migrator";
31
32
  import { bigserial, boolean, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique, uniqueIndex } from "drizzle-orm/pg-core";
33
+ import { SignJWT, jwtVerify } from "jose";
32
34
  import cors from "@fastify/cors";
33
35
  import rateLimit from "@fastify/rate-limit";
34
36
  import fastifyStatic from "@fastify/static";
35
37
  import websocket from "@fastify/websocket";
36
38
  import Fastify from "fastify";
37
- import { SignJWT, jwtVerify } from "jose";
39
+ import { promisify } from "node:util";
40
+ import matter from "gray-matter";
38
41
  import { Client, EventDispatcher, LoggerLevel, WSClient } from "@larksuiteoapi/node-sdk";
39
42
  //#region ../client/dist/observability-B4kO005X.mjs
40
43
  var import_pino = /* @__PURE__ */ __toESM(require_pino(), 1);
@@ -832,6 +835,104 @@ z.object({
832
835
  limit: z.coerce.number().int().min(1).max(100).default(20),
833
836
  cursor: z.string().optional()
834
837
  });
838
+ const contextTreeSnapshotStatusSchema = z.enum([
839
+ "active",
840
+ "stale",
841
+ "unavailable"
842
+ ]);
843
+ const contextTreeStatusSeveritySchema = z.enum([
844
+ "ok",
845
+ "warning",
846
+ "error"
847
+ ]);
848
+ const contextTreeStatusSchema = z.object({
849
+ label: z.string(),
850
+ detail: z.string().nullable(),
851
+ severity: contextTreeStatusSeveritySchema
852
+ });
853
+ const contextTreeNodeKindSchema = z.enum([
854
+ "root",
855
+ "domain",
856
+ "subdomain",
857
+ "leaf"
858
+ ]);
859
+ const contextTreeChangeTypeSchema = z.enum([
860
+ "added",
861
+ "edited",
862
+ "removed"
863
+ ]);
864
+ const contextTreeEdgeKindSchema = z.enum([
865
+ "parent",
866
+ "soft_link",
867
+ "markdown_link"
868
+ ]);
869
+ const contextTreeRiskLevelSchema = z.enum([
870
+ "low",
871
+ "medium",
872
+ "high"
873
+ ]);
874
+ const contextTreeNodeSchema = z.object({
875
+ id: z.string(),
876
+ path: z.string(),
877
+ sourcePath: z.string().nullable(),
878
+ title: z.string(),
879
+ kind: contextTreeNodeKindSchema,
880
+ owners: z.array(z.string()),
881
+ parentId: z.string().nullable(),
882
+ preview: z.string().nullable(),
883
+ relatedNodeIds: z.array(z.string()),
884
+ affectedContextArea: z.string(),
885
+ changeType: contextTreeChangeTypeSchema.nullable(),
886
+ changedAtCommit: z.string().nullable()
887
+ });
888
+ const contextTreeEdgeSchema = z.object({
889
+ source: z.string(),
890
+ target: z.string(),
891
+ kind: contextTreeEdgeKindSchema
892
+ });
893
+ const contextTreeChangeSchema = z.object({
894
+ path: z.string(),
895
+ nodeId: z.string().nullable(),
896
+ type: contextTreeChangeTypeSchema,
897
+ commit: z.string().nullable(),
898
+ changedAt: z.string().nullable(),
899
+ changedBy: z.string().nullable(),
900
+ summary: z.string().nullable()
901
+ });
902
+ const contextTreeUpdateSchema = z.object({
903
+ id: z.string(),
904
+ nodeId: z.string().nullable(),
905
+ path: z.string(),
906
+ title: z.string(),
907
+ changeType: contextTreeChangeTypeSchema,
908
+ affectedContextArea: z.string(),
909
+ reason: z.string(),
910
+ summary: z.string(),
911
+ changedBy: z.string().nullable(),
912
+ owners: z.array(z.string()),
913
+ relatedNodeIds: z.array(z.string()),
914
+ sourceCommit: z.string().nullable(),
915
+ riskLevel: contextTreeRiskLevelSchema
916
+ });
917
+ const contextTreeSummarySchema = z.object({
918
+ addedCount: z.number().int().nonnegative(),
919
+ editedCount: z.number().int().nonnegative(),
920
+ removedCount: z.number().int().nonnegative(),
921
+ changedNodeCount: z.number().int().nonnegative()
922
+ });
923
+ z.object({
924
+ repo: z.string().nullable(),
925
+ branch: z.string().nullable(),
926
+ headCommit: z.string().nullable(),
927
+ syncedAt: z.string().nullable(),
928
+ snapshotStatus: contextTreeSnapshotStatusSchema,
929
+ contextStatus: contextTreeStatusSchema,
930
+ summary: contextTreeSummarySchema,
931
+ updates: z.array(contextTreeUpdateSchema),
932
+ nodes: z.array(contextTreeNodeSchema),
933
+ edges: z.array(contextTreeEdgeSchema),
934
+ changes: z.array(contextTreeChangeSchema)
935
+ });
835
936
  /**
836
937
  * MIME types the web + client image paths recognise. Kept in sync with
837
938
  * Claude's vision API (see packages/client/src/handlers/claude-code.ts).
@@ -1584,10 +1685,11 @@ defineConfig({
1584
1685
  connectTokenExpiry: field(z.string().default("10m"), { env: "FIRST_TREE_HUB_AUTH_CONNECT_TOKEN_EXPIRY" })
1585
1686
  },
1586
1687
  contextTree: optional({
1587
- repo: field(z.string(), {
1688
+ repo: field(z.string().optional(), {
1588
1689
  env: "FIRST_TREE_HUB_CONTEXT_TREE_REPO",
1589
1690
  prompt: { message: "Context Tree repo URL (e.g. https://github.com/org/first-tree):" }
1590
1691
  }),
1692
+ localPath: field(z.string().optional(), { env: "FIRST_TREE_HUB_CONTEXT_TREE_PATH" }),
1591
1693
  branch: field(z.string().default("main"))
1592
1694
  }),
1593
1695
  github: {
@@ -1610,6 +1712,7 @@ defineConfig({
1610
1712
  max: field(z.number().default(100), { env: "FIRST_TREE_HUB_RATE_LIMIT_MAX" }),
1611
1713
  loginMax: field(z.number().default(5), { env: "FIRST_TREE_HUB_RATE_LIMIT_LOGIN_MAX" }),
1612
1714
  webhookMax: field(z.number().default(60), { env: "FIRST_TREE_HUB_RATE_LIMIT_WEBHOOK_MAX" }),
1715
+ contextTreeSnapshotMax: field(z.number().default(6), { env: "FIRST_TREE_HUB_RATE_LIMIT_CONTEXT_TREE_SNAPSHOT_MAX" }),
1613
1716
  agentMessageMax: field(z.number().default(30), { env: "FIRST_TREE_HUB_RATE_LIMIT_AGENT_MESSAGE_MAX" })
1614
1717
  }),
1615
1718
  ws: optional({ maxPayload: field(z.number().int().min(1024).default(262144), { env: "FIRST_TREE_HUB_WS_MAX_PAYLOAD" }) }),
@@ -1706,10 +1809,12 @@ var FirstTreeHubSDK = class {
1706
1809
  _baseUrl;
1707
1810
  getAccessToken;
1708
1811
  _agentId;
1812
+ _userAgent;
1709
1813
  constructor(config) {
1710
1814
  this._baseUrl = config.serverUrl.replace(/\/+$/, "");
1711
1815
  this.getAccessToken = config.getAccessToken;
1712
1816
  this._agentId = config.agentId;
1817
+ this._userAgent = config.userAgent;
1713
1818
  }
1714
1819
  /** Server base URL (without trailing slash). */
1715
1820
  get serverUrl() {
@@ -1761,7 +1866,12 @@ var FirstTreeHubSDK = class {
1761
1866
  async isHubReachable(timeoutMs = 3e3) {
1762
1867
  try {
1763
1868
  const url = `${this._baseUrl}/api/v1/health`;
1764
- return (await fetch(url, { signal: AbortSignal.timeout(timeoutMs) })).ok;
1869
+ const headers = {};
1870
+ if (this._userAgent) headers["User-Agent"] = this._userAgent;
1871
+ return (await fetch(url, {
1872
+ signal: AbortSignal.timeout(timeoutMs),
1873
+ headers
1874
+ })).ok;
1765
1875
  } catch {
1766
1876
  return false;
1767
1877
  }
@@ -1820,6 +1930,7 @@ var FirstTreeHubSDK = class {
1820
1930
  const url = `${this._baseUrl}${path}`;
1821
1931
  const headers = { Authorization: `Bearer ${await this.getAccessToken()}` };
1822
1932
  if (this._agentId) headers[AGENT_SELECTOR_HEADER] = this._agentId;
1933
+ if (this._userAgent) headers["User-Agent"] = this._userAgent;
1823
1934
  if (init?.body) headers["Content-Type"] = "application/json";
1824
1935
  const timeout = AbortSignal.timeout(FETCH_TIMEOUT_MS);
1825
1936
  const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
@@ -1929,6 +2040,7 @@ var ClientConnection = class extends EventEmitter {
1929
2040
  clientId;
1930
2041
  serverUrl;
1931
2042
  sdkVersion;
2043
+ userAgent;
1932
2044
  getAccessToken;
1933
2045
  ws = null;
1934
2046
  wsConnectTimer = null;
@@ -1982,6 +2094,7 @@ var ClientConnection = class extends EventEmitter {
1982
2094
  this.clientId = config.clientId ?? process.env.FIRST_TREE_HUB_CLIENT_ID ?? `client_${randomUUID().slice(0, 8)}`;
1983
2095
  this.serverUrl = config.serverUrl.replace(/\/+$/, "");
1984
2096
  this.sdkVersion = config.sdkVersion;
2097
+ this.userAgent = config.userAgent;
1985
2098
  this.getAccessToken = config.getAccessToken;
1986
2099
  this.wsLogger = createLogger("ws").child({ clientId: this.clientId });
1987
2100
  this.authLogger = createLogger("auth").child({ clientId: this.clientId });
@@ -2123,7 +2236,7 @@ var ClientConnection = class extends EventEmitter {
2123
2236
  return new Promise((resolve, reject) => {
2124
2237
  const wsUrl = `${this.serverUrl.replace(/^http/, "ws")}/api/v1/agent/ws/client`;
2125
2238
  this.wsLogger.info({ url: wsUrl }, "connecting");
2126
- const ws = new WebSocket(wsUrl);
2239
+ const ws = new WebSocket(wsUrl, this.userAgent ? { headers: { "User-Agent": this.userAgent } } : void 0);
2127
2240
  let settled = false;
2128
2241
  const settle = (fn, value) => {
2129
2242
  if (settled) return;
@@ -2274,7 +2387,8 @@ var ClientConnection = class extends EventEmitter {
2274
2387
  const sdk = new FirstTreeHubSDK({
2275
2388
  serverUrl: this.serverUrl,
2276
2389
  getAccessToken: this.getAccessToken,
2277
- agentId
2390
+ agentId,
2391
+ userAgent: this.userAgent
2278
2392
  });
2279
2393
  const agent = {
2280
2394
  agentId,
@@ -5960,71 +6074,6 @@ function sleep(ms) {
5960
6074
  return new Promise((resolve) => setTimeout(resolve, ms));
5961
6075
  }
5962
6076
  //#endregion
5963
- //#region src/core/output.ts
5964
- /**
5965
- * Print layer — the only place CLI code should write to stdout/stderr.
5966
- *
5967
- * Contract:
5968
- * - `print.result(data)` / `print.fail(...)` emit machine-readable JSON on
5969
- * stdout / stderr respectively. Scripts pipe into `jq` and expect a clean
5970
- * envelope, so nothing else may touch stdout.
5971
- * - `print.status` / `print.check` / `print.blank` / `print.line` are
5972
- * human-friendly and go to stderr so they never pollute a redirected stdout.
5973
- * In `--json` mode they are silenced — scripted consumers only care about
5974
- * the envelope.
5975
- */
5976
- let jsonMode = false;
5977
- function setJsonMode(enabled) {
5978
- jsonMode = enabled;
5979
- }
5980
- function result(data) {
5981
- process.stdout.write(`${JSON.stringify({
5982
- ok: true,
5983
- data
5984
- })}\n`);
5985
- }
5986
- function fail$1(code, message, exitCode = 1) {
5987
- process.stderr.write(`${JSON.stringify({
5988
- ok: false,
5989
- error: {
5990
- code,
5991
- message
5992
- }
5993
- })}\n`);
5994
- process.exit(exitCode);
5995
- }
5996
- function status(label, message) {
5997
- if (jsonMode) return;
5998
- process.stderr.write(` ${label.padEnd(20)} ${message}\n`);
5999
- }
6000
- function check(pass, label, detail = "") {
6001
- if (jsonMode) return;
6002
- const icon = pass ? "✓" : "✗";
6003
- const tail = detail ? ` ${detail}` : "";
6004
- process.stderr.write(` ${icon} ${label.padEnd(22)}${tail}\n`);
6005
- }
6006
- function blank() {
6007
- if (jsonMode) return;
6008
- process.stderr.write("\n");
6009
- }
6010
- /**
6011
- * Generic stderr writer for pre-formatted human text (multi-line tables,
6012
- * interactive prompts). Prefer `status` / `check` when the text fits; this
6013
- * exists so the `--json` mode gate can silence arbitrary human chatter.
6014
- */
6015
- function line(text) {
6016
- if (jsonMode) return;
6017
- process.stderr.write(text);
6018
- }
6019
- const print = {
6020
- result,
6021
- fail: fail$1,
6022
- status,
6023
- check,
6024
- blank,
6025
- line
6026
- };
6027
- //#endregion
6028
6077
  //#region src/cli/output.ts
6029
6078
  /**
6030
6079
  * CLI output re-exports. The underlying implementation lives in
@@ -6389,6 +6438,7 @@ var ClientRuntime = class {
6389
6438
  serverUrl,
6390
6439
  clientId,
6391
6440
  sdkVersion: options.currentVersion,
6441
+ userAgent: CLI_USER_AGENT,
6392
6442
  getAccessToken: (opts) => ensureFreshAccessToken(opts)
6393
6443
  });
6394
6444
  registerBuiltinHandlers();
@@ -7543,7 +7593,7 @@ async function checkServerHealth() {
7543
7593
  const port = get(config, "server.port") ?? 8e3;
7544
7594
  const url = `http://${host}:${port}/healthz`;
7545
7595
  try {
7546
- const res = await fetch(url, { signal: AbortSignal.timeout(3e3) });
7596
+ const res = await cliFetch(url, { signal: AbortSignal.timeout(3e3) });
7547
7597
  if (res.ok) return {
7548
7598
  label: "Server Health",
7549
7599
  ok: true,
@@ -7594,7 +7644,7 @@ async function checkServerReachable() {
7594
7644
  detail: "not configured (FIRST_TREE_HUB_SERVER_URL or config file)"
7595
7645
  };
7596
7646
  try {
7597
- const res = await fetch(`${serverUrl}/healthz`, { signal: AbortSignal.timeout(5e3) });
7647
+ const res = await cliFetch(`${serverUrl}/healthz`, { signal: AbortSignal.timeout(5e3) });
7598
7648
  if (res.ok) return {
7599
7649
  label: "Server URL",
7600
7650
  ok: true,
@@ -7739,7 +7789,7 @@ async function checkWebSocket() {
7739
7789
  };
7740
7790
  const wsUrl = serverUrl.replace(/^http/, "ws");
7741
7791
  try {
7742
- if ((await fetch(`${serverUrl}/healthz`, { signal: AbortSignal.timeout(3e3) })).ok) return {
7792
+ if ((await cliFetch(`${serverUrl}/healthz`, { signal: AbortSignal.timeout(3e3) })).ok) return {
7743
7793
  label: "WebSocket",
7744
7794
  ok: true,
7745
7795
  detail: `${wsUrl} (server reachable)`
@@ -7829,7 +7879,7 @@ function createApiNameResolver(serverUrl, getAccessToken) {
7829
7879
  if (cache) return cache;
7830
7880
  const token = await getAccessToken();
7831
7881
  const map = /* @__PURE__ */ new Map();
7832
- const res = await fetch(`${serverUrl}/api/v1/me/managed-agents`, {
7882
+ const res = await cliFetch(`${serverUrl}/api/v1/me/managed-agents`, {
7833
7883
  method: "GET",
7834
7884
  headers: { Authorization: `Bearer ${token}` },
7835
7885
  signal: AbortSignal.timeout(1e4)
@@ -8061,7 +8111,7 @@ async function onboardCheck(args) {
8061
8111
  value: serverUrl
8062
8112
  });
8063
8113
  try {
8064
- const res = await fetch(`${serverUrl}/api/v1/health`);
8114
+ const res = await cliFetch(`${serverUrl}/api/v1/health`);
8065
8115
  items.push({
8066
8116
  key: "server_reachable",
8067
8117
  label: "Server reachable",
@@ -8133,7 +8183,7 @@ function formatCheckReport(items) {
8133
8183
  return lines.join("\n");
8134
8184
  }
8135
8185
  async function resolveDefaultOrgId$1(serverUrl, accessToken) {
8136
- const res = await fetch(`${serverUrl}/api/v1/me`, {
8186
+ const res = await cliFetch(`${serverUrl}/api/v1/me`, {
8137
8187
  headers: { Authorization: `Bearer ${accessToken}` },
8138
8188
  signal: AbortSignal.timeout(1e4)
8139
8189
  });
@@ -8145,7 +8195,7 @@ async function resolveDefaultOrgId$1(serverUrl, accessToken) {
8145
8195
  throw new Error("Multiple organizations — pass --org explicitly to onboard");
8146
8196
  }
8147
8197
  async function createAgentViaAdmin(serverUrl, accessToken, orgId, body) {
8148
- const res = await fetch(`${serverUrl}/api/v1/orgs/${encodeURIComponent(orgId)}/agents`, {
8198
+ const res = await cliFetch(`${serverUrl}/api/v1/orgs/${encodeURIComponent(orgId)}/agents`, {
8149
8199
  method: "POST",
8150
8200
  headers: {
8151
8201
  Authorization: `Bearer ${accessToken}`,
@@ -8204,7 +8254,7 @@ async function onboardCreate(args) {
8204
8254
  }
8205
8255
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
8206
8256
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
8207
- const { bindFeishuBot } = await import("./feishu-AEMHwT6L.mjs").then((n) => n.r);
8257
+ const { bindFeishuBot } = await import("./feishu-Th_-ivJ7.mjs").then((n) => n.r);
8208
8258
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
8209
8259
  if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
8210
8260
  else {
@@ -8304,7 +8354,7 @@ async function promptAddAgent(opts = {}) {
8304
8354
  validate: (v) => v.length > 0 ? true : "Agent UUID is required"
8305
8355
  });
8306
8356
  const token = await ensureFreshAccessToken();
8307
- const res = await fetch(`${serverUrl}/api/v1/agents/${encodeURIComponent(agentId)}`, {
8357
+ const res = await cliFetch(`${serverUrl}/api/v1/agents/${encodeURIComponent(agentId)}`, {
8308
8358
  headers: { Authorization: `Bearer ${token}` },
8309
8359
  signal: AbortSignal.timeout(1e4)
8310
8360
  });
@@ -8376,7 +8426,7 @@ function setNestedByDot(obj, dotPath, value) {
8376
8426
  * value (the in-band repair path catches any remaining drift on first bind).
8377
8427
  */
8378
8428
  async function reconcileLocalRuntimeProviders(opts) {
8379
- const res = await fetch(`${opts.serverUrl}/api/v1/me/pinned-agents`, { headers: { Authorization: `Bearer ${opts.accessToken}` } });
8429
+ const res = await cliFetch(`${opts.serverUrl}/api/v1/me/pinned-agents`, { headers: { Authorization: `Bearer ${opts.accessToken}` } });
8380
8430
  if (!res.ok) throw new Error(`hub returned ${res.status} on /clients/me/agents`);
8381
8431
  const items = await res.json();
8382
8432
  const byAgentId = new Map(items.map((it) => [it.agentId, it]));
@@ -8416,7 +8466,7 @@ async function reconcileLocalRuntimeProviders(opts) {
8416
8466
  * client startup since capabilities only matter for UI / admin checks.
8417
8467
  */
8418
8468
  async function uploadClientCapabilities(opts) {
8419
- const res = await fetch(`${opts.serverUrl}/api/v1/clients/${encodeURIComponent(opts.clientId)}/capabilities`, {
8469
+ const res = await cliFetch(`${opts.serverUrl}/api/v1/clients/${encodeURIComponent(opts.clientId)}/capabilities`, {
8420
8470
  method: "PATCH",
8421
8471
  headers: {
8422
8472
  Authorization: `Bearer ${opts.accessToken}`,
@@ -8960,7 +9010,7 @@ function formatErrorTraces(traces) {
8960
9010
  ].join("\n");
8961
9011
  }
8962
9012
  const SUBSCRIBERS_PATH = "subscribers.json";
8963
- function normalize(email) {
9013
+ function normalize$1(email) {
8964
9014
  return email.toLowerCase().trim();
8965
9015
  }
8966
9016
  async function readSubscribers(config, branch) {
@@ -8993,7 +9043,7 @@ async function writeSubscribers(config, branch, data, sha, message) {
8993
9043
  */
8994
9044
  async function addSubscriber(config, issueNumber, email, options = {}) {
8995
9045
  const branch = options.branch ?? "hearback-data";
8996
- const normalized = normalize(email);
9046
+ const normalized = normalize$1(email);
8997
9047
  const key = String(issueNumber);
8998
9048
  await ensureBranch(config, branch);
8999
9049
  for (let attempt = 0; attempt < 2; attempt++) {
@@ -9417,7 +9467,7 @@ function createFeedbackHandler(config) {
9417
9467
  return { handle };
9418
9468
  }
9419
9469
  //#endregion
9420
- //#region ../server/dist/app-CsaZ6IX8.mjs
9470
+ //#region ../server/dist/app-EvpSNDM6.mjs
9421
9471
  var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
9422
9472
  init_esm();
9423
9473
  var __defProp = Object.defineProperty;
@@ -13551,8 +13601,12 @@ function clientWsRoutes(notifier, instanceId) {
13551
13601
  return async (app) => {
13552
13602
  const jwtSecretBytes = new TextEncoder().encode(app.config.secrets.jwtSecret);
13553
13603
  const inboxMaxInFlightPerAgent = app.config.inbox?.maxInFlightPerAgent ?? DEFAULT_INBOX_MAX_IN_FLIGHT_PER_AGENT;
13554
- app.get("/client", { websocket: true }, async (socket) => {
13555
- startWsConnectionSpan(socket);
13604
+ app.get("/client", { websocket: true }, async (socket, request) => {
13605
+ const ua = request.headers["user-agent"];
13606
+ startWsConnectionSpan(socket, {
13607
+ remoteIp: request.ip,
13608
+ userAgent: typeof ua === "string" ? ua.slice(0, 200) : void 0
13609
+ });
13556
13610
  let session = null;
13557
13611
  let jwtDefaultOrgId = null;
13558
13612
  let clientId = null;
@@ -14464,8 +14518,12 @@ async function refreshAccessToken(db, refreshToken, jwtSecretKey, expiries) {
14464
14518
  try {
14465
14519
  const { payload: p } = await jwtVerify(refreshToken, secret);
14466
14520
  payload = p;
14467
- } catch {
14468
- throw new UnauthorizedError("Invalid or expired refresh token", { "auth.refresh.reason": "jwt_verify_failed" });
14521
+ } catch (err) {
14522
+ const untrusted = decodeJwtForTrace(refreshToken);
14523
+ throw new UnauthorizedError("Invalid or expired refresh token", {
14524
+ "auth.refresh.reason": classifyJoseError(err),
14525
+ ...untrustedAttrs("auth.refresh", untrusted)
14526
+ });
14469
14527
  }
14470
14528
  if (payload.type !== "refresh" || !payload.sub) throw new UnauthorizedError("Invalid token type", {
14471
14529
  "auth.refresh.reason": "wrong_token_type",
@@ -14516,10 +14574,17 @@ async function exchangeConnectToken(db, connectToken, jwtSecretKey, expiries) {
14516
14574
  try {
14517
14575
  const { payload: p } = await jwtVerify(connectToken, secret);
14518
14576
  payload = p;
14519
- } catch {
14520
- throw new UnauthorizedError("Invalid or expired connect token");
14577
+ } catch (err) {
14578
+ const untrusted = decodeJwtForTrace(connectToken);
14579
+ throw new UnauthorizedError("Invalid or expired connect token", {
14580
+ "auth.connect.reason": classifyJoseError(err),
14581
+ ...untrustedAttrs("auth.connect", untrusted)
14582
+ });
14521
14583
  }
14522
- if (payload.type !== "connect" || !payload.sub) throw new UnauthorizedError("Invalid token type — expected connect token");
14584
+ if (payload.type !== "connect" || !payload.sub) throw new UnauthorizedError("Invalid token type — expected connect token", {
14585
+ "auth.connect.reason": "wrong_token_type",
14586
+ "auth.connect.actual_type": String(payload.type ?? "<missing>")
14587
+ });
14523
14588
  const jti = payload.jti;
14524
14589
  if (jti) {
14525
14590
  if (consumedConnectJtis.has(jti)) throw new UnauthorizedError("Connect token has already been used");
@@ -15934,6 +15999,707 @@ async function contextTreeInfoRoutes(app) {
15934
15999
  };
15935
16000
  });
15936
16001
  }
16002
+ const execFileAsync = promisify(execFile);
16003
+ const ROOT_NODE_ID = "root";
16004
+ const NODE_FILE = "NODE.md";
16005
+ const EMPTY_TREE_COMMIT = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
16006
+ const MAX_DIFF_ENTRIES = 200;
16007
+ const MAX_MARKDOWN_FILES = 1e3;
16008
+ const MAX_MARKDOWN_FILE_BYTES = 512 * 1024;
16009
+ const SNAPSHOT_CACHE_TTL_MS = 3e4;
16010
+ const GIT_TIMEOUT_MS = 5e3;
16011
+ const GIT_MAX_BUFFER = 10 * 1024 * 1024;
16012
+ const GIT_LOG_RECORD_SEPARATOR = "";
16013
+ const CONTEXT_TREE_SNAPSHOT_WINDOWS = {
16014
+ ONE_DAY: "1d",
16015
+ SEVEN_DAYS: "7d",
16016
+ THIRTY_DAYS: "30d"
16017
+ };
16018
+ const WINDOW_DAYS = {
16019
+ "1d": 1,
16020
+ "7d": 7,
16021
+ "30d": 30
16022
+ };
16023
+ const snapshotCache = /* @__PURE__ */ new Map();
16024
+ async function getContextTreeSnapshot(config, window = CONTEXT_TREE_SNAPSHOT_WINDOWS.SEVEN_DAYS) {
16025
+ const repo = config.contextTree?.repo ?? null;
16026
+ const branch = config.contextTree?.branch ?? null;
16027
+ const resolved = resolveContextTreeRoot(repo, config.contextTree?.localPath);
16028
+ if (!resolved.root) return unavailableSnapshot(repo, branch, resolved.reason);
16029
+ const now = (/* @__PURE__ */ new Date()).toISOString();
16030
+ try {
16031
+ const headCommit = await gitOutput(resolved.root, ["rev-parse", "HEAD"]);
16032
+ const actualBranch = await safeGitOutput(resolved.root, [
16033
+ "rev-parse",
16034
+ "--abbrev-ref",
16035
+ "HEAD"
16036
+ ]);
16037
+ if (branch && actualBranch && actualBranch !== branch) return unavailableSnapshot(repo, actualBranch, `Context Tree checkout is on branch "${actualBranch}", but server config expects "${branch}".`);
16038
+ const comparisonBaseCommit = await comparisonBaseForWindow(resolved.root, window);
16039
+ const cacheKey = snapshotCacheKey(resolved.root, actualBranch ?? branch, headCommit, comparisonBaseCommit, window);
16040
+ const cached = snapshotCache.get(cacheKey);
16041
+ if (cached && cached.expiresAt > Date.now()) return {
16042
+ ...cached.snapshot,
16043
+ syncedAt: now
16044
+ };
16045
+ const tree = buildTree(await readMarkdownFiles(resolved.root));
16046
+ const diffResult = comparisonBaseCommit ? await readDiffEntries(resolved.root, comparisonBaseCommit, headCommit) : {
16047
+ entries: [],
16048
+ truncated: false
16049
+ };
16050
+ const changes = buildChanges(diffResult.entries, tree);
16051
+ const nodesWithGhosts = addRemovedGhostNodes(applyChangesToNodes(tree.nodes, changes), changes);
16052
+ const summary = summarizeChanges(changes);
16053
+ const updates = buildUpdates(changes, nodesWithGhosts);
16054
+ const statusWarning = contextStatusWarning(diffResult.truncated);
16055
+ const snapshot = {
16056
+ repo,
16057
+ branch: actualBranch ?? branch,
16058
+ headCommit,
16059
+ syncedAt: now,
16060
+ snapshotStatus: "active",
16061
+ contextStatus: {
16062
+ label: statusWarning ? "Team context needs attention" : "Team context is current",
16063
+ detail: statusWarning ?? "Agents have a synced team context snapshot available.",
16064
+ severity: statusWarning ? "warning" : "ok"
16065
+ },
16066
+ summary,
16067
+ updates,
16068
+ nodes: nodesWithGhosts,
16069
+ edges: tree.edges,
16070
+ changes
16071
+ };
16072
+ snapshotCache.set(cacheKey, {
16073
+ expiresAt: Date.now() + SNAPSHOT_CACHE_TTL_MS,
16074
+ snapshot
16075
+ });
16076
+ return snapshot;
16077
+ } catch (error) {
16078
+ return unavailableSnapshot(repo, branch, error instanceof Error ? error.message : "Unable to read Context Tree snapshot");
16079
+ }
16080
+ }
16081
+ function snapshotCacheKey(root, branch, headCommit, comparisonBase, window) {
16082
+ return [
16083
+ root,
16084
+ branch ?? "unknown",
16085
+ headCommit,
16086
+ comparisonBase ?? "none",
16087
+ window
16088
+ ].join(":");
16089
+ }
16090
+ function contextStatusWarning(truncated) {
16091
+ if (truncated) return `Showing the first ${MAX_DIFF_ENTRIES} changed files.`;
16092
+ return null;
16093
+ }
16094
+ function resolveContextTreeRoot(repo, localPath) {
16095
+ const candidate = localPath && localPath.trim().length > 0 ? localPath : repo;
16096
+ if (!candidate) return {
16097
+ root: null,
16098
+ reason: "Context Tree is not configured."
16099
+ };
16100
+ const normalized = candidate.startsWith("file://") ? candidate.slice(7) : candidate;
16101
+ const root = isAbsolute(normalized) ? normalize(normalized) : resolve(process.cwd(), normalized);
16102
+ if (existsSync(root)) return {
16103
+ root,
16104
+ reason: "ok"
16105
+ };
16106
+ if (/^https?:\/\//.test(normalized) || /^[^/]+\/[^/]+$/.test(normalized)) return {
16107
+ root: null,
16108
+ reason: "Context Tree repo is configured as a remote URL. Set FIRST_TREE_HUB_CONTEXT_TREE_PATH to a readable local checkout for this version."
16109
+ };
16110
+ return {
16111
+ root: null,
16112
+ reason: `Context Tree checkout not found at ${root}.`
16113
+ };
16114
+ }
16115
+ function unavailableSnapshot(repo, branch, detail) {
16116
+ return {
16117
+ repo,
16118
+ branch,
16119
+ headCommit: null,
16120
+ syncedAt: null,
16121
+ snapshotStatus: "unavailable",
16122
+ contextStatus: {
16123
+ label: "Team context unavailable",
16124
+ detail,
16125
+ severity: "error"
16126
+ },
16127
+ summary: {
16128
+ addedCount: 0,
16129
+ editedCount: 0,
16130
+ removedCount: 0,
16131
+ changedNodeCount: 0
16132
+ },
16133
+ updates: [],
16134
+ nodes: [],
16135
+ edges: [],
16136
+ changes: []
16137
+ };
16138
+ }
16139
+ async function gitOutput(cwd, args) {
16140
+ const { stdout } = await execFileAsync("git", args, {
16141
+ cwd,
16142
+ timeout: GIT_TIMEOUT_MS,
16143
+ maxBuffer: GIT_MAX_BUFFER
16144
+ });
16145
+ return stdout.trim();
16146
+ }
16147
+ async function safeGitOutput(cwd, args) {
16148
+ try {
16149
+ return await gitOutput(cwd, args);
16150
+ } catch {
16151
+ return null;
16152
+ }
16153
+ }
16154
+ async function readMarkdownFiles(root) {
16155
+ const paths = (await walkMarkdown(root, root)).slice(0, MAX_MARKDOWN_FILES);
16156
+ return (await Promise.all(paths.map(async (path) => {
16157
+ const absolutePath = join(root, path);
16158
+ if ((await stat(absolutePath)).size > MAX_MARKDOWN_FILE_BYTES) return null;
16159
+ return {
16160
+ relativePath: path,
16161
+ parsed: parseMarkdown(await readFile(absolutePath, "utf8"))
16162
+ };
16163
+ }))).filter((file) => file !== null);
16164
+ }
16165
+ function parseMarkdown(raw) {
16166
+ try {
16167
+ const parsed = matter(raw);
16168
+ return {
16169
+ content: parsed.content,
16170
+ data: parsed.data
16171
+ };
16172
+ } catch {
16173
+ return parseMarkdownFallback(raw);
16174
+ }
16175
+ }
16176
+ function parseMarkdownFallback(raw) {
16177
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(raw);
16178
+ if (!match) return {
16179
+ content: raw,
16180
+ data: {}
16181
+ };
16182
+ const frontmatter = match[1] ?? "";
16183
+ const data = {};
16184
+ for (const line of frontmatter.split(/\r?\n/)) {
16185
+ const field = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line.trim());
16186
+ if (!field) continue;
16187
+ const key = field[1];
16188
+ const value = field[2] ?? "";
16189
+ if (key === "title") {
16190
+ data.title = value.replace(/^["']|["']$/g, "");
16191
+ continue;
16192
+ }
16193
+ if (key === "owners" || key === "soft_links") data[key] = parseInlineStringList(value);
16194
+ }
16195
+ return {
16196
+ content: raw.slice(match[0].length),
16197
+ data
16198
+ };
16199
+ }
16200
+ function parseInlineStringList(value) {
16201
+ const trimmed = value.trim();
16202
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return [];
16203
+ return trimmed.slice(1, -1).split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter((item) => item.length > 0);
16204
+ }
16205
+ async function walkMarkdown(root, current) {
16206
+ const entries = await readdir(current, { withFileTypes: true });
16207
+ const paths = [];
16208
+ for (const entry of entries) {
16209
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
16210
+ const absolute = join(current, entry.name);
16211
+ if (entry.isDirectory()) {
16212
+ paths.push(...await walkMarkdown(root, absolute));
16213
+ continue;
16214
+ }
16215
+ if (entry.isFile() && extname(entry.name).toLowerCase() === ".md") paths.push(toPosix(relative(root, absolute)));
16216
+ }
16217
+ paths.sort((a, b) => a.localeCompare(b));
16218
+ return paths;
16219
+ }
16220
+ function buildTree(files) {
16221
+ const nodeBySourcePath = /* @__PURE__ */ new Map();
16222
+ const nodeByTreePath = /* @__PURE__ */ new Map();
16223
+ const dirNodeContent = /* @__PURE__ */ new Map();
16224
+ const leafFiles = [];
16225
+ const directories = new Set([""]);
16226
+ for (const file of files) {
16227
+ const dir = sourceDir(file.relativePath);
16228
+ addDirectoryAncestors(directories, dir);
16229
+ if (file.relativePath.endsWith(`/${NODE_FILE}`) || file.relativePath === NODE_FILE) dirNodeContent.set(dir, file);
16230
+ else leafFiles.push(file);
16231
+ }
16232
+ const rootNode = {
16233
+ id: ROOT_NODE_ID,
16234
+ path: "",
16235
+ sourcePath: NODE_FILE,
16236
+ title: titleFromFile(dirNodeContent.get("")?.parsed.data, "Context Tree"),
16237
+ kind: "root",
16238
+ owners: ownersFromFile(dirNodeContent.get("")?.parsed.data),
16239
+ parentId: null,
16240
+ preview: previewFromContent(dirNodeContent.get("")?.parsed.content ?? ""),
16241
+ relatedNodeIds: [],
16242
+ affectedContextArea: "root",
16243
+ changeType: null,
16244
+ changedAtCommit: null
16245
+ };
16246
+ const nodes = [rootNode];
16247
+ nodeByTreePath.set("", rootNode);
16248
+ if (dirNodeContent.has("")) nodeBySourcePath.set(NODE_FILE, rootNode);
16249
+ const sortedDirs = [...directories].filter((dir) => dir.length > 0).sort((a, b) => a.localeCompare(b));
16250
+ for (const dir of sortedDirs) {
16251
+ const source = dirNodeContent.get(dir);
16252
+ const node = {
16253
+ id: dirNodeId(dir),
16254
+ path: dir,
16255
+ sourcePath: source?.relativePath ?? null,
16256
+ title: titleFromFile(source?.parsed.data, titleFromPath(dir)),
16257
+ kind: kindForDirectory(dir),
16258
+ owners: ownersFromFile(source?.parsed.data),
16259
+ parentId: parentDir(dir) ? dirNodeId(parentDir(dir)) : ROOT_NODE_ID,
16260
+ preview: previewFromContent(source?.parsed.content ?? ""),
16261
+ relatedNodeIds: [],
16262
+ affectedContextArea: contextAreaFromPath(dir),
16263
+ changeType: null,
16264
+ changedAtCommit: null
16265
+ };
16266
+ nodes.push(node);
16267
+ nodeByTreePath.set(dir, node);
16268
+ if (source) nodeBySourcePath.set(source.relativePath, node);
16269
+ }
16270
+ for (const file of leafFiles) {
16271
+ const treePath = stripMarkdownExtension(file.relativePath);
16272
+ const dir = sourceDir(file.relativePath);
16273
+ const node = {
16274
+ id: fileNodeId(file.relativePath),
16275
+ path: treePath,
16276
+ sourcePath: file.relativePath,
16277
+ title: titleFromFile(file.parsed.data, titleFromPath(treePath)),
16278
+ kind: "leaf",
16279
+ owners: ownersFromFile(file.parsed.data),
16280
+ parentId: dir ? dirNodeId(dir) : ROOT_NODE_ID,
16281
+ preview: previewFromContent(file.parsed.content),
16282
+ relatedNodeIds: [],
16283
+ affectedContextArea: contextAreaFromPath(treePath),
16284
+ changeType: null,
16285
+ changedAtCommit: null
16286
+ };
16287
+ nodes.push(node);
16288
+ nodeByTreePath.set(treePath, node);
16289
+ nodeBySourcePath.set(file.relativePath, node);
16290
+ }
16291
+ const edges = parentEdges(nodes);
16292
+ const relatedEdges = relatedEdgesForFiles(files, nodeByTreePath, nodeBySourcePath);
16293
+ const relatedByNode = /* @__PURE__ */ new Map();
16294
+ for (const edge of relatedEdges) {
16295
+ if (!relatedByNode.has(edge.source)) relatedByNode.set(edge.source, /* @__PURE__ */ new Set());
16296
+ relatedByNode.get(edge.source)?.add(edge.target);
16297
+ }
16298
+ return {
16299
+ nodes: nodes.map((node) => ({
16300
+ ...node,
16301
+ relatedNodeIds: [...relatedByNode.get(node.id) ?? /* @__PURE__ */ new Set()]
16302
+ })),
16303
+ edges: [...edges, ...relatedEdges],
16304
+ nodeBySourcePath,
16305
+ nodeByTreePath
16306
+ };
16307
+ }
16308
+ function parentEdges(nodes) {
16309
+ return nodes.filter((node) => node.parentId).map((node) => ({
16310
+ source: node.parentId ?? ROOT_NODE_ID,
16311
+ target: node.id,
16312
+ kind: "parent"
16313
+ }));
16314
+ }
16315
+ function relatedEdgesForFiles(files, nodeByTreePath, nodeBySourcePath) {
16316
+ const edges = [];
16317
+ const seen = /* @__PURE__ */ new Set();
16318
+ for (const file of files) {
16319
+ const sourceNode = nodeBySourcePath.get(file.relativePath);
16320
+ if (!sourceNode) continue;
16321
+ const softLinks = stringArrayField(file.parsed.data, "soft_links");
16322
+ for (const link of softLinks) {
16323
+ const target = resolveLinkedNode(link, file.relativePath, nodeByTreePath);
16324
+ if (!target) continue;
16325
+ const key = `${sourceNode.id}:soft_link:${target.id}`;
16326
+ if (seen.has(key)) continue;
16327
+ seen.add(key);
16328
+ edges.push({
16329
+ source: sourceNode.id,
16330
+ target: target.id,
16331
+ kind: "soft_link"
16332
+ });
16333
+ }
16334
+ for (const link of markdownLinks(file.parsed.content)) {
16335
+ const target = resolveLinkedNode(link, file.relativePath, nodeByTreePath);
16336
+ if (!target) continue;
16337
+ const key = `${sourceNode.id}:markdown_link:${target.id}`;
16338
+ if (seen.has(key)) continue;
16339
+ seen.add(key);
16340
+ edges.push({
16341
+ source: sourceNode.id,
16342
+ target: target.id,
16343
+ kind: "markdown_link"
16344
+ });
16345
+ }
16346
+ }
16347
+ return edges;
16348
+ }
16349
+ async function comparisonBaseForWindow(root, window) {
16350
+ const commitBeforeWindow = await safeGitOutput(root, [
16351
+ "rev-list",
16352
+ "-1",
16353
+ `--before=${(/* @__PURE__ */ new Date(Date.now() - WINDOW_DAYS[window] * 24 * 60 * 60 * 1e3)).toISOString()}`,
16354
+ "HEAD"
16355
+ ]);
16356
+ return commitBeforeWindow && commitBeforeWindow.length > 0 ? commitBeforeWindow : EMPTY_TREE_COMMIT;
16357
+ }
16358
+ async function readDiffEntries(root, comparisonBase, headCommit) {
16359
+ if (!isSafeCommit(comparisonBase) || !isSafeCommit(headCommit)) return {
16360
+ entries: [],
16361
+ truncated: false
16362
+ };
16363
+ try {
16364
+ const output = await gitOutput(root, [
16365
+ "diff",
16366
+ "--name-status",
16367
+ "-M",
16368
+ comparisonBase,
16369
+ "HEAD",
16370
+ "--",
16371
+ "*.md"
16372
+ ]);
16373
+ if (!output) return {
16374
+ entries: [],
16375
+ truncated: false
16376
+ };
16377
+ const pendingEntries = [];
16378
+ for (const line of output.split("\n")) {
16379
+ if (pendingEntries.length >= MAX_DIFF_ENTRIES) break;
16380
+ const parts = line.split(" ").filter(Boolean);
16381
+ const status = parts[0];
16382
+ if (!status) continue;
16383
+ if (status.startsWith("R")) {
16384
+ const oldPath = parts[1];
16385
+ const newPath = parts[2];
16386
+ if (oldPath) pendingEntries.push({
16387
+ type: "removed",
16388
+ path: toPosix(oldPath)
16389
+ });
16390
+ if (pendingEntries.length >= MAX_DIFF_ENTRIES) break;
16391
+ if (newPath) pendingEntries.push({
16392
+ type: "added",
16393
+ path: toPosix(newPath)
16394
+ });
16395
+ continue;
16396
+ }
16397
+ const path = parts[1];
16398
+ if (!path) continue;
16399
+ if (status === "A") pendingEntries.push({
16400
+ type: "added",
16401
+ path: toPosix(path)
16402
+ });
16403
+ if (status === "M") pendingEntries.push({
16404
+ type: "edited",
16405
+ path: toPosix(path)
16406
+ });
16407
+ if (status === "D") pendingEntries.push({
16408
+ type: "removed",
16409
+ path: toPosix(path)
16410
+ });
16411
+ }
16412
+ const metadataByPath = await readChangeMetadataByPath(root, comparisonBase, headCommit, pendingEntries.map((entry) => entry.path));
16413
+ const entries = pendingEntries.map((entry) => ({
16414
+ ...entry,
16415
+ ...metadataByPath.get(entry.path) ?? fallbackChangeMetadata(headCommit)
16416
+ }));
16417
+ return {
16418
+ entries,
16419
+ truncated: output.split("\n").filter(Boolean).length > entries.length
16420
+ };
16421
+ } catch {
16422
+ return {
16423
+ entries: [],
16424
+ truncated: false
16425
+ };
16426
+ }
16427
+ }
16428
+ async function readChangeMetadataByPath(root, comparisonBase, headCommit, paths) {
16429
+ const uniquePaths = [...new Set(paths)];
16430
+ const metadataByPath = /* @__PURE__ */ new Map();
16431
+ if (uniquePaths.length === 0) return metadataByPath;
16432
+ const output = await safeGitOutput(root, [
16433
+ "log",
16434
+ "--name-only",
16435
+ `--format=${GIT_LOG_RECORD_SEPARATOR}%H%x00%cI%x00%an%x00%s`,
16436
+ `${comparisonBase}..HEAD`,
16437
+ "--",
16438
+ ...uniquePaths
16439
+ ]);
16440
+ if (!output) return metadataByPath;
16441
+ for (const rawRecord of output.split(GIT_LOG_RECORD_SEPARATOR)) {
16442
+ const record = rawRecord.trim();
16443
+ if (!record) continue;
16444
+ const newlineIndex = record.indexOf("\n");
16445
+ const header = newlineIndex === -1 ? record : record.slice(0, newlineIndex);
16446
+ const changedPaths = newlineIndex === -1 ? [] : record.slice(newlineIndex + 1).split("\n").map((path) => toPosix(path.trim())).filter((path) => path.length > 0);
16447
+ const fields = header.split("\0");
16448
+ const commit = fields[0];
16449
+ if (!commit || !isSafeCommit(commit)) continue;
16450
+ const metadata = {
16451
+ commit,
16452
+ changedAt: fields[1] && fields[1].length > 0 ? fields[1] : null,
16453
+ changedBy: fields[2] && fields[2].length > 0 ? fields[2] : null,
16454
+ summary: cleanCommitSubject(fields[3] ?? null)
16455
+ };
16456
+ for (const changedPath of changedPaths) if (!metadataByPath.has(changedPath)) metadataByPath.set(changedPath, metadata);
16457
+ }
16458
+ for (const path of uniquePaths) if (!metadataByPath.has(path)) metadataByPath.set(path, fallbackChangeMetadata(headCommit));
16459
+ return metadataByPath;
16460
+ }
16461
+ function fallbackChangeMetadata(headCommit) {
16462
+ return {
16463
+ commit: headCommit,
16464
+ changedAt: null,
16465
+ changedBy: null,
16466
+ summary: null
16467
+ };
16468
+ }
16469
+ function isSafeCommit(value) {
16470
+ return /^[0-9a-f]{40}$/i.test(value);
16471
+ }
16472
+ function cleanCommitSubject(subject) {
16473
+ if (!subject) return null;
16474
+ const cleaned = subject.trim().replace(/^(feat|fix|docs|chore|refactor|test|style|perf|ci|build)(\([^)]+\))?:\s*/i, "").replace(/\s+/g, " ");
16475
+ if (cleaned.length < 12) return null;
16476
+ if (cleaned.length > 140) return `${cleaned.slice(0, 137)}...`;
16477
+ if (/^(merge|wip|update|updated|change|changes)$/i.test(cleaned)) return null;
16478
+ return cleaned;
16479
+ }
16480
+ function buildChanges(entries, tree) {
16481
+ return entries.map((entry) => {
16482
+ const node = tree.nodeBySourcePath.get(entry.path) ?? tree.nodeByTreePath.get(stripMarkdownExtension(entry.path));
16483
+ return {
16484
+ path: entry.path,
16485
+ nodeId: node?.id ?? ghostNodeId(entry.path),
16486
+ type: entry.type,
16487
+ commit: entry.commit,
16488
+ changedAt: entry.changedAt,
16489
+ changedBy: entry.changedBy,
16490
+ summary: entry.summary
16491
+ };
16492
+ });
16493
+ }
16494
+ function applyChangesToNodes(nodes, changes) {
16495
+ const changeByNode = /* @__PURE__ */ new Map();
16496
+ for (const change of changes) if (change.nodeId) changeByNode.set(change.nodeId, change);
16497
+ return nodes.map((node) => {
16498
+ const change = changeByNode.get(node.id);
16499
+ if (!change) return node;
16500
+ return {
16501
+ ...node,
16502
+ changeType: change.type,
16503
+ changedAtCommit: change.commit
16504
+ };
16505
+ });
16506
+ }
16507
+ function addRemovedGhostNodes(nodes, changes) {
16508
+ const nodeIds = new Set(nodes.map((node) => node.id));
16509
+ const ghosts = [];
16510
+ for (const change of changes) {
16511
+ if (change.type !== "removed" || !change.nodeId || nodeIds.has(change.nodeId)) continue;
16512
+ const treePath = stripMarkdownExtension(change.path);
16513
+ const dir = sourceDir(change.path);
16514
+ const parentNodeId = dir ? dirNodeId(dir) : ROOT_NODE_ID;
16515
+ ghosts.push({
16516
+ id: change.nodeId,
16517
+ path: treePath,
16518
+ sourcePath: change.path,
16519
+ title: titleFromPath(treePath),
16520
+ kind: "leaf",
16521
+ owners: [],
16522
+ parentId: nodeIds.has(parentNodeId) ? parentNodeId : ROOT_NODE_ID,
16523
+ preview: null,
16524
+ relatedNodeIds: [],
16525
+ affectedContextArea: contextAreaFromPath(treePath),
16526
+ changeType: "removed",
16527
+ changedAtCommit: change.commit
16528
+ });
16529
+ }
16530
+ return [...nodes, ...ghosts];
16531
+ }
16532
+ function summarizeChanges(changes) {
16533
+ let addedCount = 0;
16534
+ let editedCount = 0;
16535
+ let removedCount = 0;
16536
+ for (const change of changes) {
16537
+ if (change.type === "added") addedCount += 1;
16538
+ if (change.type === "edited") editedCount += 1;
16539
+ if (change.type === "removed") removedCount += 1;
16540
+ }
16541
+ return {
16542
+ addedCount,
16543
+ editedCount,
16544
+ removedCount,
16545
+ changedNodeCount: changes.length
16546
+ };
16547
+ }
16548
+ function buildUpdates(changes, nodes) {
16549
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
16550
+ const updates = changes.map((change) => {
16551
+ const node = change.nodeId ? nodeById.get(change.nodeId) : void 0;
16552
+ const path = node?.path ?? stripMarkdownExtension(change.path);
16553
+ const title = node?.title ?? titleFromPath(path);
16554
+ const affectedContextArea = node?.affectedContextArea ?? contextAreaFromPath(path);
16555
+ return {
16556
+ id: `update:${change.type}:${change.path}`,
16557
+ nodeId: change.nodeId,
16558
+ path,
16559
+ title,
16560
+ changeType: change.type,
16561
+ affectedContextArea,
16562
+ reason: reasonForUpdate(change.type, affectedContextArea),
16563
+ summary: changeSummaryForUpdate(change, node),
16564
+ changedBy: change.changedBy,
16565
+ owners: node?.owners ?? [],
16566
+ relatedNodeIds: node?.relatedNodeIds ?? [],
16567
+ sourceCommit: change.commit,
16568
+ riskLevel: riskLevelForChange(change.type, node?.kind)
16569
+ };
16570
+ });
16571
+ updates.sort((a, b) => updateRank(a) - updateRank(b) || a.path.localeCompare(b.path));
16572
+ return updates;
16573
+ }
16574
+ function changeSummaryForUpdate(change, node) {
16575
+ if (change.summary) return change.summary;
16576
+ if (change.type === "added") return "added this team knowledge";
16577
+ if (change.type === "removed") return "removed this team knowledge";
16578
+ return node ? `updated ${node.title}` : "updated this team knowledge";
16579
+ }
16580
+ function updateRank(update) {
16581
+ const depth = update.path.split("/").filter(Boolean).length;
16582
+ const specificityRank = depth >= 2 ? 0 : depth === 1 ? 1 : 2;
16583
+ const typeRank = update.changeType === "removed" ? 0 : update.changeType === "added" ? 1 : 2;
16584
+ return specificityRank * 10 + typeRank;
16585
+ }
16586
+ function riskLevelForChange(changeType, kind) {
16587
+ if (changeType === "removed") return "high";
16588
+ if (kind === "root" || kind === "domain" || kind === "subdomain") return "medium";
16589
+ return "low";
16590
+ }
16591
+ function reasonForUpdate(changeType, affectedContextArea) {
16592
+ if (changeType === "added") return `Agents can use new team knowledge when working on ${affectedContextArea}.`;
16593
+ if (changeType === "removed") return `Agents should stop using the old team knowledge for ${affectedContextArea}.`;
16594
+ return `Agents can use updated team knowledge when working on ${affectedContextArea}.`;
16595
+ }
16596
+ function titleFromFile(data, fallback) {
16597
+ return stringField(data, "title") ?? fallback;
16598
+ }
16599
+ function ownersFromFile(data) {
16600
+ return stringArrayField(data, "owners");
16601
+ }
16602
+ function stringField(data, key) {
16603
+ if (!isRecord$1(data)) return null;
16604
+ const value = data[key];
16605
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
16606
+ }
16607
+ function stringArrayField(data, key) {
16608
+ if (!isRecord$1(data)) return [];
16609
+ const value = data[key];
16610
+ if (!Array.isArray(value)) return [];
16611
+ return value.filter((item) => typeof item === "string" && item.trim().length > 0);
16612
+ }
16613
+ function isRecord$1(value) {
16614
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16615
+ }
16616
+ function previewFromContent(content) {
16617
+ const normalized = content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).join(" ");
16618
+ if (!normalized) return null;
16619
+ return normalized.length > 240 ? `${normalized.slice(0, 237)}...` : normalized;
16620
+ }
16621
+ function markdownLinks(content) {
16622
+ const links = [];
16623
+ const re = /\[[^\]]+\]\(([^)]+)\)/g;
16624
+ let match = re.exec(content);
16625
+ while (match) {
16626
+ const target = match[1];
16627
+ if (target && !/^https?:\/\//.test(target) && !target.startsWith("#")) links.push(target.split("#")[0] ?? target);
16628
+ match = re.exec(content);
16629
+ }
16630
+ return links;
16631
+ }
16632
+ function resolveLinkedNode(link, fromSourcePath, nodeByTreePath) {
16633
+ const withoutAnchor = link.split("#")[0] ?? link;
16634
+ if (!withoutAnchor) return null;
16635
+ const baseDir = sourceDir(fromSourcePath);
16636
+ const cleaned = (withoutAnchor.startsWith("/") ? withoutAnchor.slice(1) : toPosix(normalize(join(baseDir, withoutAnchor)))).replace(/^\.\//, "");
16637
+ const candidates = [
16638
+ stripMarkdownExtension(cleaned),
16639
+ stripMarkdownExtension(cleaned.replace(/\/NODE\.md$/i, "")),
16640
+ cleaned.replace(/\/$/g, "")
16641
+ ].filter((candidate) => candidate.length > 0);
16642
+ for (const candidate of candidates) {
16643
+ const node = nodeByTreePath.get(candidate);
16644
+ if (node) return node;
16645
+ }
16646
+ return null;
16647
+ }
16648
+ function addDirectoryAncestors(directories, dir) {
16649
+ if (!dir) return;
16650
+ const parts = dir.split("/");
16651
+ for (let i = 1; i <= parts.length; i += 1) directories.add(parts.slice(0, i).join("/"));
16652
+ }
16653
+ function kindForDirectory(dir) {
16654
+ return dir.includes("/") ? "subdomain" : "domain";
16655
+ }
16656
+ function sourceDir(path) {
16657
+ const dir = toPosix(dirname(path));
16658
+ return dir === "." ? "" : dir;
16659
+ }
16660
+ function parentDir(dir) {
16661
+ const parent = toPosix(dirname(dir));
16662
+ return parent === "." ? "" : parent;
16663
+ }
16664
+ function stripMarkdownExtension(path) {
16665
+ return path.replace(/\/NODE\.md$/i, "").replace(/\.md$/i, "");
16666
+ }
16667
+ function titleFromPath(path) {
16668
+ return (path.split("/").filter(Boolean).at(-1) ?? "Context Tree").replace(/[-_]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()).trim();
16669
+ }
16670
+ function contextAreaFromPath(path) {
16671
+ const parts = path.split("/").filter(Boolean);
16672
+ if (parts.length === 0) return "root";
16673
+ return parts.map((part) => part.replace(/[-_]+/g, " ")).join(" / ");
16674
+ }
16675
+ function dirNodeId(dir) {
16676
+ return dir ? `dir:${dir}` : ROOT_NODE_ID;
16677
+ }
16678
+ function fileNodeId(path) {
16679
+ return `file:${path}`;
16680
+ }
16681
+ function ghostNodeId(path) {
16682
+ return `removed:${path}`;
16683
+ }
16684
+ function toPosix(path) {
16685
+ return sep === "/" ? path : path.split(sep).join("/");
16686
+ }
16687
+ const querySchema = z.object({ window: z.enum([
16688
+ "1d",
16689
+ "7d",
16690
+ "30d"
16691
+ ]).optional() }).strict();
16692
+ async function contextTreeSnapshotRoutes(app) {
16693
+ app.get("/snapshot", { config: { rateLimit: {
16694
+ max: app.config.rateLimit?.contextTreeSnapshotMax ?? 6,
16695
+ timeWindow: "1 minute",
16696
+ keyGenerator: (request) => request.user?.userId ?? request.ip
16697
+ } } }, async (request) => {
16698
+ const query = querySchema.parse(request.query);
16699
+ const snapshot = await getContextTreeSnapshot(app.config, query.window ?? "7d");
16700
+ return contextTreeSnapshotSchema.parse(snapshot);
16701
+ });
16702
+ }
15937
16703
  /**
15938
16704
  * Resolve the client IP for rate-limit attribution.
15939
16705
  *
@@ -16035,7 +16801,7 @@ async function healthzRoutes(app) {
16035
16801
  * `api/orgs/invitations.ts` (Class B, admin-gated).
16036
16802
  */
16037
16803
  async function publicInvitationRoutes(app) {
16038
- const { previewInvitation } = await import("./invitation-DWlyNb8x-DZTW9I26.mjs");
16804
+ const { previewInvitation } = await import("./invitation-DWlyNb8x-D3zjZSwI.mjs");
16039
16805
  app.get("/:token/preview", async (request, reply) => {
16040
16806
  if (!request.params.token) throw new UnauthorizedError("Token required");
16041
16807
  const preview = await previewInvitation(app.db, request.params.token);
@@ -16132,9 +16898,35 @@ async function meRoutes(app) {
16132
16898
  */
16133
16899
  app.get("/me/pinned-agents", async (request) => {
16134
16900
  const { userId } = requireUser(request);
16135
- const { listMyPinnedAgents } = await import("./client-m1OM4Iag-HKWgB3Yk.mjs");
16901
+ const { listMyPinnedAgents } = await import("./client-By1K4VVT-C5K7WZo6.mjs");
16136
16902
  return listMyPinnedAgents(app.db, { userId });
16137
16903
  });
16904
+ /**
16905
+ * GET /me/clients — cross-org list of every client owned by the caller.
16906
+ * A client is owned by exactly one user (clients.user_id) and the same
16907
+ * machine can carry agents from any org the user belongs to, so this
16908
+ * surface is org-agnostic — Class A by the decision tree in
16909
+ * `docs/http-path-conventions.md`. Powers Settings → Computers in the
16910
+ * web UI; the org-admin audit view (`/orgs/:orgId/clients`) stays for
16911
+ * a future "team device audit" surface.
16912
+ */
16913
+ app.get("/me/clients", async (request) => {
16914
+ const { userId } = requireUser(request);
16915
+ const list = await listClients(app.db, { userId });
16916
+ const refreshExpirySeconds = expiryToSeconds(app.config.auth.refreshTokenExpiry);
16917
+ return list.map((c) => ({
16918
+ id: c.id,
16919
+ userId: c.userId,
16920
+ status: c.status,
16921
+ authState: deriveAuthState(c, refreshExpirySeconds),
16922
+ sdkVersion: c.sdkVersion,
16923
+ hostname: c.hostname,
16924
+ os: c.os,
16925
+ agentCount: c.agentCount,
16926
+ connectedAt: serializeDate(c.connectedAt),
16927
+ lastSeenAt: c.lastSeenAt.toISOString()
16928
+ }));
16929
+ });
16138
16930
  app.get("/me/organizations", async (request) => {
16139
16931
  const { userId } = requireUser(request);
16140
16932
  return (await listActiveMemberships(app.db, userId)).map((r) => ({
@@ -16931,7 +17723,11 @@ function orgWsRoutes(notifier, jwtSecret) {
16931
17723
  }
16932
17724
  return async (app) => {
16933
17725
  app.get("/", { websocket: true }, async (socket, request) => {
16934
- startWsConnectionSpan(socket, { remoteIp: request.ip });
17726
+ const ua = request.headers["user-agent"];
17727
+ startWsConnectionSpan(socket, {
17728
+ remoteIp: request.ip,
17729
+ userAgent: typeof ua === "string" ? ua.slice(0, 200) : void 0
17730
+ });
16935
17731
  const orgIdFromPath = request.params.orgId;
16936
17732
  const token = request.query.token;
16937
17733
  if (!token || !orgIdFromPath) {
@@ -17702,8 +18498,12 @@ function userAuthHook(db, jwtSecret) {
17702
18498
  try {
17703
18499
  const { payload: p } = await jwtVerify(token, secret);
17704
18500
  payload = p;
17705
- } catch {
17706
- throw new UnauthorizedError("Invalid or expired token", { "auth.failure_reason": "jwt_verify_failed" });
18501
+ } catch (err) {
18502
+ const untrusted = decodeJwtForTrace(token);
18503
+ throw new UnauthorizedError("Invalid or expired token", {
18504
+ "auth.failure_reason": classifyJoseError(err),
18505
+ ...untrustedAttrs("auth", untrusted)
18506
+ });
17707
18507
  }
17708
18508
  if (payload.type !== "access" || !payload.sub) throw new UnauthorizedError("Invalid token type", {
17709
18509
  "auth.failure_reason": "wrong_token_type",
@@ -18907,7 +19707,7 @@ function createPulseAggregator(options) {
18907
19707
  * Returning a string (rather than undefined) keeps the welcome frame well-
18908
19708
  * formed — the client treats the value advisorily.
18909
19709
  */
18910
- function resolveCommandVersion$1(injected) {
19710
+ function resolveCommandVersion(injected) {
18911
19711
  if (injected && injected.trim().length > 0) return injected;
18912
19712
  try {
18913
19713
  const pkg = createRequire(import.meta.url)("../package.json");
@@ -18999,7 +19799,7 @@ async function buildApp(config) {
18999
19799
  const db = connectDatabase(config.database.url);
19000
19800
  app.decorate("db", db);
19001
19801
  app.decorate("config", config);
19002
- const commandVersion = resolveCommandVersion$1(config.commandVersion);
19802
+ const commandVersion = resolveCommandVersion(config.commandVersion);
19003
19803
  app.decorate("commandVersion", commandVersion);
19004
19804
  app.log.info({ commandVersion }, "Hub server advertising command version");
19005
19805
  const listenClient = postgres(config.database.url, { max: 1 });
@@ -19014,7 +19814,8 @@ async function buildApp(config) {
19014
19814
  await app.register(rateLimit, {
19015
19815
  max: config.rateLimit?.max ?? 100,
19016
19816
  timeWindow: "1 minute",
19017
- hook: "preHandler"
19817
+ hook: "preHandler",
19818
+ errorResponseBuilder: buildRateLimitError
19018
19819
  });
19019
19820
  app.addHook("onSend", bodyCaptureOnSendHook);
19020
19821
  const userAuth = userAuthHook(db, config.secrets.jwtSecret);
@@ -19088,6 +19889,9 @@ async function buildApp(config) {
19088
19889
  await api.register(publicInvitationRoutes, { prefix: "/invitations" });
19089
19890
  await api.register(contextTreeInfoRoutes, { prefix: "/context-tree" });
19090
19891
  await api.register(bootstrapConfigRoutes, { prefix: "/bootstrap" });
19892
+ await api.register(userScope("contextTreeScope", async (scope) => {
19893
+ await scope.register(contextTreeSnapshotRoutes);
19894
+ }), { prefix: "/context-tree" });
19091
19895
  await api.register(userScope("meRoutesScope", async (scope) => {
19092
19896
  await scope.register(meRoutes);
19093
19897
  }), { prefix: "" });
@@ -19151,12 +19955,11 @@ async function buildApp(config) {
19151
19955
  if (webDistPath) {
19152
19956
  const webRoot = resolve(webDistPath);
19153
19957
  if (existsSync(webRoot)) {
19154
- await app.register(fastifyStatic, {
19155
- root: webRoot,
19156
- wildcard: false
19157
- });
19958
+ await app.register(fastifyStatic, { root: webRoot });
19158
19959
  app.setNotFoundHandler((request, reply) => {
19159
19960
  if (request.url.startsWith("/api/")) return reply.status(404).send({ error: "Not found" });
19961
+ const requestPath = request.url.split("?")[0] ?? request.url;
19962
+ if (requestPath.startsWith("/assets/") || extname(requestPath).length > 0) return reply.status(404).send({ error: "Not found" });
19160
19963
  return reply.sendFile("index.html");
19161
19964
  });
19162
19965
  }
@@ -19211,58 +20014,6 @@ async function buildApp(config) {
19211
20014
  return app;
19212
20015
  }
19213
20016
  //#endregion
19214
- //#region src/core/version.ts
19215
- /**
19216
- * Version of the consumer-facing `@agent-team-foundation/first-tree-hub`
19217
- * package. Read once at module load so the CLI, client runtime, and server
19218
- * bootstrap all quote the same string.
19219
- *
19220
- * Path-based lookups (`require("../../package.json")`) do not survive the
19221
- * tsdown bundle: the source lives at `src/core/version.ts` but every
19222
- * emitted chunk lands in `dist/` — shifting the relative depth by one and
19223
- * pointing at `packages/package.json` instead of our own manifest (the
19224
- * v0.9.1 "Cannot find module ../../package.json" crash). Walk up from this
19225
- * module's URL and accept the first `package.json` whose `name` matches, so
19226
- * dev runs (`tsx src/cli/index.ts`) and the published bundle
19227
- * (`dist/cli/index.mjs`) both resolve the same file.
19228
- */
19229
- const PACKAGE_NAME$1 = "@agent-team-foundation/first-tree-hub";
19230
- /**
19231
- * Sentinel returned when the walker exhausts every parent directory without
19232
- * finding our manifest. Deliberately NOT valid SemVer so the client-side
19233
- * `UpdateManager` drops into its `semver.valid(current) === false` warn-and-
19234
- * skip branch instead of treating it as `< target` and triggering a spurious
19235
- * self-update loop (the scenario where the startup crash this module fixes
19236
- * would otherwise quietly reincarnate as repeated `npm install -g @latest`).
19237
- */
19238
- const UNRESOLVED_VERSION = "unknown";
19239
- /**
19240
- * Exported for tests. Walks up from `moduleUrl`'s directory looking for a
19241
- * `package.json` whose `name` field equals {@link PACKAGE_NAME}. Returns
19242
- * {@link UNRESOLVED_VERSION} as a last-resort fallback so the CLI never
19243
- * crashes on a missing manifest.
19244
- */
19245
- function resolveCommandVersion(moduleUrl = import.meta.url) {
19246
- let dir = dirname(fileURLToPath(moduleUrl));
19247
- for (let i = 0; i < 10; i++) {
19248
- try {
19249
- const pkg = JSON.parse(readFileSync(resolve(dir, "package.json"), "utf8"));
19250
- if (pkg.name === PACKAGE_NAME$1 && typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
19251
- } catch (err) {
19252
- const code = err.code;
19253
- if (code !== "ENOENT" && code !== "ENOTDIR") {
19254
- const message = err instanceof Error ? err.message : String(err);
19255
- print.line(`[first-tree-hub] warning: could not read ${dir}/package.json: ${message}\n`);
19256
- }
19257
- }
19258
- const parent = dirname(dir);
19259
- if (parent === dir) break;
19260
- dir = parent;
19261
- }
19262
- return UNRESOLVED_VERSION;
19263
- }
19264
- const COMMAND_VERSION = resolveCommandVersion();
19265
- //#endregion
19266
20017
  //#region src/core/server.ts
19267
20018
  /**
19268
20019
  * Full server start orchestration:
@@ -19317,7 +20068,7 @@ async function startServer(options) {
19317
20068
  instanceId: `srv_${randomUUID().slice(0, 8)}`,
19318
20069
  commandVersion: COMMAND_VERSION
19319
20070
  };
19320
- const { initTelemetry, shutdownTelemetry } = await import("./observability-Co8OO0og.mjs");
20071
+ const { initTelemetry, shutdownTelemetry } = await import("./observability-CYsdAcoF.mjs");
19321
20072
  await initTelemetry(serverConfig.observability.tracing, config.instanceId);
19322
20073
  const app = await buildApp(config);
19323
20074
  const SHUTDOWN_FORCE_EXIT_MS = 8e3;
@@ -19678,7 +20429,7 @@ async function promptReplaceOrCancel(newMemberId) {
19678
20429
  }) === "replace" ? "proceed" : "cancel";
19679
20430
  }
19680
20431
  async function exchangeToken(url, token) {
19681
- const res = await fetch(`${url}/api/v1/auth/connect-token`, {
20432
+ const res = await cliFetch(`${url}/api/v1/auth/connect-token`, {
19682
20433
  method: "POST",
19683
20434
  headers: { "Content-Type": "application/json" },
19684
20435
  body: JSON.stringify({ token }),
@@ -19790,4 +20541,4 @@ function registerSaaSConnectCommand(program) {
19790
20541
  });
19791
20542
  }
19792
20543
  //#endregion
19793
- export { findStaleAliases as $, checkDatabase as A, installClientService as B, runHomeMigration as C, checkAgentConfigs as D, runMigrations as E, checkServerReachable as F, stopClientService as G, resolveCliInvocation as H, checkWebSocket as I, isDockerAvailable as J, uninstallClientService as K, printResults as L, checkNodeVersion as M, checkServerConfig as N, checkBackgroundService as O, checkServerHealth as P, rotateClientIdWithBackup as Q, reconcileAgentConfigs as R, saveOnboardState as S, migrateLocalAgentDirs as T, restartClientService as U, isServiceSupported as V, startClientService as W, ClientRuntime as X, stopPostgres as Y, handleClientOrgMismatch as Z, promptMissingFields as _, probeCapabilities as _t, declineUpdate as a, fail as at, onboardCheck as b, detectInstallMode as c, print as ct, startServer as d, ClientOrgMismatchError as dt, formatStaleReason as et, COMMAND_VERSION as f, ClientUserMismatchError as ft, promptAddAgent as g, cleanWorkspaces as gt, isInteractive as h, SessionRegistry as ht, createExecuteUpdate as i, resolveReplyToFromEnv as it, checkDocker as j, checkClientConfig as k, fetchLatestVersion as l, setJsonMode as lt, uploadClientCapabilities as m, SdkError as mt, deriveHubUrlFromToken as n, createOwner as nt, promptUpdate as o, success as ot, reconcileLocalRuntimeProviders as p, FirstTreeHubSDK as pt, ensurePostgres as q, registerSaaSConnectCommand as r, hasUser as rt, PACKAGE_NAME as s, blank as st, HubUrlDerivationError as t, removeLocalAgent as tt, installGlobalLatest as u, status as ut, formatCheckReport as v, applyClientLoggerConfig as vt, createApiNameResolver as w, onboardCreate as x, loadOnboardState as y, configureClientLoggerForService as yt, getClientServiceStatus as z };
20544
+ export { formatStaleReason as $, checkDocker as A, isServiceSupported as B, createApiNameResolver as C, checkBackgroundService as D, checkAgentConfigs as E, checkWebSocket as F, uninstallClientService as G, restartClientService as H, printResults as I, stopPostgres as J, ensurePostgres as K, reconcileAgentConfigs as L, checkServerConfig as M, checkServerHealth as N, checkClientConfig as O, checkServerReachable as P, findStaleAliases as Q, getClientServiceStatus as R, runHomeMigration as S, runMigrations as T, startClientService as U, resolveCliInvocation as V, stopClientService as W, handleClientOrgMismatch as X, ClientRuntime as Y, rotateClientIdWithBackup as Z, formatCheckReport as _, declineUpdate as a, success as at, onboardCreate as b, detectInstallMode as c, FirstTreeHubSDK as ct, startServer as d, cleanWorkspaces as dt, removeLocalAgent as et, reconcileLocalRuntimeProviders as f, probeCapabilities as ft, promptMissingFields as g, promptAddAgent as h, createExecuteUpdate as i, fail as it, checkNodeVersion as j, checkDatabase as k, fetchLatestVersion as l, SdkError as lt, isInteractive as m, configureClientLoggerForService as mt, deriveHubUrlFromToken as n, hasUser as nt, promptUpdate as o, ClientOrgMismatchError as ot, uploadClientCapabilities as p, applyClientLoggerConfig as pt, isDockerAvailable as q, registerSaaSConnectCommand as r, resolveReplyToFromEnv as rt, PACKAGE_NAME as s, ClientUserMismatchError as st, HubUrlDerivationError as t, createOwner as tt, installGlobalLatest as u, SessionRegistry as ut, loadOnboardState as v, migrateLocalAgentDirs as w, saveOnboardState as x, onboardCheck as y, installClientService as z };