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

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,24 +1,24 @@
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";
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-Bk7FX689.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-B-FRMuvL.mjs";
4
+ 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-FuUBFTEB.mjs";
5
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";
6
+ 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-B416INrm.mjs";
7
7
  import { n as init_esm, r as trace, t as esm_exports } from "./esm-iadMkGbV.mjs";
8
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";
9
9
  import { createRequire } from "node:module";
10
10
  import { ZodError, z } from "zod";
11
- import { basename, delimiter, dirname, isAbsolute, join, resolve } from "node:path";
11
+ import { basename, delimiter, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
12
12
  import { Writable } from "node:stream";
13
13
  import { homedir, hostname, platform, tmpdir, userInfo } from "node:os";
14
14
  import { EventEmitter } from "node:events";
15
15
  import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync, writeSync } from "node:fs";
16
16
  import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
17
17
  import WebSocket from "ws";
18
- import { mkdir, writeFile } from "node:fs/promises";
18
+ import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
19
19
  import { parse, stringify } from "yaml";
20
20
  import { query } from "@anthropic-ai/claude-agent-sdk";
21
- import { execFileSync, execSync, spawn, spawnSync } from "node:child_process";
21
+ import { execFile, execFileSync, execSync, spawn, spawnSync } from "node:child_process";
22
22
  import { Codex } from "@openai/codex-sdk";
23
23
  import { fileURLToPath } from "node:url";
24
24
  import * as semver from "semver";
@@ -35,6 +35,8 @@ import fastifyStatic from "@fastify/static";
35
35
  import websocket from "@fastify/websocket";
36
36
  import Fastify from "fastify";
37
37
  import { SignJWT, jwtVerify } from "jose";
38
+ import { promisify } from "node:util";
39
+ import matter from "gray-matter";
38
40
  import { Client, EventDispatcher, LoggerLevel, WSClient } from "@larksuiteoapi/node-sdk";
39
41
  //#region ../client/dist/observability-B4kO005X.mjs
40
42
  var import_pino = /* @__PURE__ */ __toESM(require_pino(), 1);
@@ -832,6 +834,104 @@ z.object({
832
834
  limit: z.coerce.number().int().min(1).max(100).default(20),
833
835
  cursor: z.string().optional()
834
836
  });
837
+ const contextTreeSnapshotStatusSchema = z.enum([
838
+ "active",
839
+ "stale",
840
+ "unavailable"
841
+ ]);
842
+ const contextTreeStatusSeveritySchema = z.enum([
843
+ "ok",
844
+ "warning",
845
+ "error"
846
+ ]);
847
+ const contextTreeStatusSchema = z.object({
848
+ label: z.string(),
849
+ detail: z.string().nullable(),
850
+ severity: contextTreeStatusSeveritySchema
851
+ });
852
+ const contextTreeNodeKindSchema = z.enum([
853
+ "root",
854
+ "domain",
855
+ "subdomain",
856
+ "leaf"
857
+ ]);
858
+ const contextTreeChangeTypeSchema = z.enum([
859
+ "added",
860
+ "edited",
861
+ "removed"
862
+ ]);
863
+ const contextTreeEdgeKindSchema = z.enum([
864
+ "parent",
865
+ "soft_link",
866
+ "markdown_link"
867
+ ]);
868
+ const contextTreeRiskLevelSchema = z.enum([
869
+ "low",
870
+ "medium",
871
+ "high"
872
+ ]);
873
+ const contextTreeNodeSchema = z.object({
874
+ id: z.string(),
875
+ path: z.string(),
876
+ sourcePath: z.string().nullable(),
877
+ title: z.string(),
878
+ kind: contextTreeNodeKindSchema,
879
+ owners: z.array(z.string()),
880
+ parentId: z.string().nullable(),
881
+ preview: z.string().nullable(),
882
+ relatedNodeIds: z.array(z.string()),
883
+ affectedContextArea: z.string(),
884
+ changeType: contextTreeChangeTypeSchema.nullable(),
885
+ changedAtCommit: z.string().nullable()
886
+ });
887
+ const contextTreeEdgeSchema = z.object({
888
+ source: z.string(),
889
+ target: z.string(),
890
+ kind: contextTreeEdgeKindSchema
891
+ });
892
+ const contextTreeChangeSchema = z.object({
893
+ path: z.string(),
894
+ nodeId: z.string().nullable(),
895
+ type: contextTreeChangeTypeSchema,
896
+ commit: z.string().nullable(),
897
+ changedAt: z.string().nullable(),
898
+ changedBy: z.string().nullable(),
899
+ summary: z.string().nullable()
900
+ });
901
+ const contextTreeUpdateSchema = z.object({
902
+ id: z.string(),
903
+ nodeId: z.string().nullable(),
904
+ path: z.string(),
905
+ title: z.string(),
906
+ changeType: contextTreeChangeTypeSchema,
907
+ affectedContextArea: z.string(),
908
+ reason: z.string(),
909
+ summary: z.string(),
910
+ changedBy: z.string().nullable(),
911
+ owners: z.array(z.string()),
912
+ relatedNodeIds: z.array(z.string()),
913
+ sourceCommit: z.string().nullable(),
914
+ riskLevel: contextTreeRiskLevelSchema
915
+ });
916
+ const contextTreeSummarySchema = z.object({
917
+ addedCount: z.number().int().nonnegative(),
918
+ editedCount: z.number().int().nonnegative(),
919
+ removedCount: z.number().int().nonnegative(),
920
+ changedNodeCount: z.number().int().nonnegative()
921
+ });
922
+ z.object({
923
+ repo: z.string().nullable(),
924
+ branch: z.string().nullable(),
925
+ headCommit: z.string().nullable(),
926
+ syncedAt: z.string().nullable(),
927
+ snapshotStatus: contextTreeSnapshotStatusSchema,
928
+ contextStatus: contextTreeStatusSchema,
929
+ summary: contextTreeSummarySchema,
930
+ updates: z.array(contextTreeUpdateSchema),
931
+ nodes: z.array(contextTreeNodeSchema),
932
+ edges: z.array(contextTreeEdgeSchema),
933
+ changes: z.array(contextTreeChangeSchema)
934
+ });
835
935
  /**
836
936
  * MIME types the web + client image paths recognise. Kept in sync with
837
937
  * Claude's vision API (see packages/client/src/handlers/claude-code.ts).
@@ -1584,10 +1684,11 @@ defineConfig({
1584
1684
  connectTokenExpiry: field(z.string().default("10m"), { env: "FIRST_TREE_HUB_AUTH_CONNECT_TOKEN_EXPIRY" })
1585
1685
  },
1586
1686
  contextTree: optional({
1587
- repo: field(z.string(), {
1687
+ repo: field(z.string().optional(), {
1588
1688
  env: "FIRST_TREE_HUB_CONTEXT_TREE_REPO",
1589
1689
  prompt: { message: "Context Tree repo URL (e.g. https://github.com/org/first-tree):" }
1590
1690
  }),
1691
+ localPath: field(z.string().optional(), { env: "FIRST_TREE_HUB_CONTEXT_TREE_PATH" }),
1591
1692
  branch: field(z.string().default("main"))
1592
1693
  }),
1593
1694
  github: {
@@ -1610,6 +1711,7 @@ defineConfig({
1610
1711
  max: field(z.number().default(100), { env: "FIRST_TREE_HUB_RATE_LIMIT_MAX" }),
1611
1712
  loginMax: field(z.number().default(5), { env: "FIRST_TREE_HUB_RATE_LIMIT_LOGIN_MAX" }),
1612
1713
  webhookMax: field(z.number().default(60), { env: "FIRST_TREE_HUB_RATE_LIMIT_WEBHOOK_MAX" }),
1714
+ contextTreeSnapshotMax: field(z.number().default(6), { env: "FIRST_TREE_HUB_RATE_LIMIT_CONTEXT_TREE_SNAPSHOT_MAX" }),
1613
1715
  agentMessageMax: field(z.number().default(30), { env: "FIRST_TREE_HUB_RATE_LIMIT_AGENT_MESSAGE_MAX" })
1614
1716
  }),
1615
1717
  ws: optional({ maxPayload: field(z.number().int().min(1024).default(262144), { env: "FIRST_TREE_HUB_WS_MAX_PAYLOAD" }) }),
@@ -8204,7 +8306,7 @@ async function onboardCreate(args) {
8204
8306
  }
8205
8307
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
8206
8308
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
8207
- const { bindFeishuBot } = await import("./feishu-AEMHwT6L.mjs").then((n) => n.r);
8309
+ const { bindFeishuBot } = await import("./feishu-GvFABWW5.mjs").then((n) => n.r);
8208
8310
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
8209
8311
  if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
8210
8312
  else {
@@ -8960,7 +9062,7 @@ function formatErrorTraces(traces) {
8960
9062
  ].join("\n");
8961
9063
  }
8962
9064
  const SUBSCRIBERS_PATH = "subscribers.json";
8963
- function normalize(email) {
9065
+ function normalize$1(email) {
8964
9066
  return email.toLowerCase().trim();
8965
9067
  }
8966
9068
  async function readSubscribers(config, branch) {
@@ -8993,7 +9095,7 @@ async function writeSubscribers(config, branch, data, sha, message) {
8993
9095
  */
8994
9096
  async function addSubscriber(config, issueNumber, email, options = {}) {
8995
9097
  const branch = options.branch ?? "hearback-data";
8996
- const normalized = normalize(email);
9098
+ const normalized = normalize$1(email);
8997
9099
  const key = String(issueNumber);
8998
9100
  await ensureBranch(config, branch);
8999
9101
  for (let attempt = 0; attempt < 2; attempt++) {
@@ -9417,7 +9519,7 @@ function createFeedbackHandler(config) {
9417
9519
  return { handle };
9418
9520
  }
9419
9521
  //#endregion
9420
- //#region ../server/dist/app-CsaZ6IX8.mjs
9522
+ //#region ../server/dist/app-Cbgqlj0e.mjs
9421
9523
  var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
9422
9524
  init_esm();
9423
9525
  var __defProp = Object.defineProperty;
@@ -15934,6 +16036,707 @@ async function contextTreeInfoRoutes(app) {
15934
16036
  };
15935
16037
  });
15936
16038
  }
16039
+ const execFileAsync = promisify(execFile);
16040
+ const ROOT_NODE_ID = "root";
16041
+ const NODE_FILE = "NODE.md";
16042
+ const EMPTY_TREE_COMMIT = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
16043
+ const MAX_DIFF_ENTRIES = 200;
16044
+ const MAX_MARKDOWN_FILES = 1e3;
16045
+ const MAX_MARKDOWN_FILE_BYTES = 512 * 1024;
16046
+ const SNAPSHOT_CACHE_TTL_MS = 3e4;
16047
+ const GIT_TIMEOUT_MS = 5e3;
16048
+ const GIT_MAX_BUFFER = 10 * 1024 * 1024;
16049
+ const GIT_LOG_RECORD_SEPARATOR = "";
16050
+ const CONTEXT_TREE_SNAPSHOT_WINDOWS = {
16051
+ ONE_DAY: "1d",
16052
+ SEVEN_DAYS: "7d",
16053
+ THIRTY_DAYS: "30d"
16054
+ };
16055
+ const WINDOW_DAYS = {
16056
+ "1d": 1,
16057
+ "7d": 7,
16058
+ "30d": 30
16059
+ };
16060
+ const snapshotCache = /* @__PURE__ */ new Map();
16061
+ async function getContextTreeSnapshot(config, window = CONTEXT_TREE_SNAPSHOT_WINDOWS.SEVEN_DAYS) {
16062
+ const repo = config.contextTree?.repo ?? null;
16063
+ const branch = config.contextTree?.branch ?? null;
16064
+ const resolved = resolveContextTreeRoot(repo, config.contextTree?.localPath);
16065
+ if (!resolved.root) return unavailableSnapshot(repo, branch, resolved.reason);
16066
+ const now = (/* @__PURE__ */ new Date()).toISOString();
16067
+ try {
16068
+ const headCommit = await gitOutput(resolved.root, ["rev-parse", "HEAD"]);
16069
+ const actualBranch = await safeGitOutput(resolved.root, [
16070
+ "rev-parse",
16071
+ "--abbrev-ref",
16072
+ "HEAD"
16073
+ ]);
16074
+ if (branch && actualBranch && actualBranch !== branch) return unavailableSnapshot(repo, actualBranch, `Context Tree checkout is on branch "${actualBranch}", but server config expects "${branch}".`);
16075
+ const comparisonBaseCommit = await comparisonBaseForWindow(resolved.root, window);
16076
+ const cacheKey = snapshotCacheKey(resolved.root, actualBranch ?? branch, headCommit, comparisonBaseCommit, window);
16077
+ const cached = snapshotCache.get(cacheKey);
16078
+ if (cached && cached.expiresAt > Date.now()) return {
16079
+ ...cached.snapshot,
16080
+ syncedAt: now
16081
+ };
16082
+ const tree = buildTree(await readMarkdownFiles(resolved.root));
16083
+ const diffResult = comparisonBaseCommit ? await readDiffEntries(resolved.root, comparisonBaseCommit, headCommit) : {
16084
+ entries: [],
16085
+ truncated: false
16086
+ };
16087
+ const changes = buildChanges(diffResult.entries, tree);
16088
+ const nodesWithGhosts = addRemovedGhostNodes(applyChangesToNodes(tree.nodes, changes), changes);
16089
+ const summary = summarizeChanges(changes);
16090
+ const updates = buildUpdates(changes, nodesWithGhosts);
16091
+ const statusWarning = contextStatusWarning(diffResult.truncated);
16092
+ const snapshot = {
16093
+ repo,
16094
+ branch: actualBranch ?? branch,
16095
+ headCommit,
16096
+ syncedAt: now,
16097
+ snapshotStatus: "active",
16098
+ contextStatus: {
16099
+ label: statusWarning ? "Team context needs attention" : "Team context is current",
16100
+ detail: statusWarning ?? "Agents have a synced team context snapshot available.",
16101
+ severity: statusWarning ? "warning" : "ok"
16102
+ },
16103
+ summary,
16104
+ updates,
16105
+ nodes: nodesWithGhosts,
16106
+ edges: tree.edges,
16107
+ changes
16108
+ };
16109
+ snapshotCache.set(cacheKey, {
16110
+ expiresAt: Date.now() + SNAPSHOT_CACHE_TTL_MS,
16111
+ snapshot
16112
+ });
16113
+ return snapshot;
16114
+ } catch (error) {
16115
+ return unavailableSnapshot(repo, branch, error instanceof Error ? error.message : "Unable to read Context Tree snapshot");
16116
+ }
16117
+ }
16118
+ function snapshotCacheKey(root, branch, headCommit, comparisonBase, window) {
16119
+ return [
16120
+ root,
16121
+ branch ?? "unknown",
16122
+ headCommit,
16123
+ comparisonBase ?? "none",
16124
+ window
16125
+ ].join(":");
16126
+ }
16127
+ function contextStatusWarning(truncated) {
16128
+ if (truncated) return `Showing the first ${MAX_DIFF_ENTRIES} changed files.`;
16129
+ return null;
16130
+ }
16131
+ function resolveContextTreeRoot(repo, localPath) {
16132
+ const candidate = localPath && localPath.trim().length > 0 ? localPath : repo;
16133
+ if (!candidate) return {
16134
+ root: null,
16135
+ reason: "Context Tree is not configured."
16136
+ };
16137
+ const normalized = candidate.startsWith("file://") ? candidate.slice(7) : candidate;
16138
+ const root = isAbsolute(normalized) ? normalize(normalized) : resolve(process.cwd(), normalized);
16139
+ if (existsSync(root)) return {
16140
+ root,
16141
+ reason: "ok"
16142
+ };
16143
+ if (/^https?:\/\//.test(normalized) || /^[^/]+\/[^/]+$/.test(normalized)) return {
16144
+ root: null,
16145
+ 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."
16146
+ };
16147
+ return {
16148
+ root: null,
16149
+ reason: `Context Tree checkout not found at ${root}.`
16150
+ };
16151
+ }
16152
+ function unavailableSnapshot(repo, branch, detail) {
16153
+ return {
16154
+ repo,
16155
+ branch,
16156
+ headCommit: null,
16157
+ syncedAt: null,
16158
+ snapshotStatus: "unavailable",
16159
+ contextStatus: {
16160
+ label: "Team context unavailable",
16161
+ detail,
16162
+ severity: "error"
16163
+ },
16164
+ summary: {
16165
+ addedCount: 0,
16166
+ editedCount: 0,
16167
+ removedCount: 0,
16168
+ changedNodeCount: 0
16169
+ },
16170
+ updates: [],
16171
+ nodes: [],
16172
+ edges: [],
16173
+ changes: []
16174
+ };
16175
+ }
16176
+ async function gitOutput(cwd, args) {
16177
+ const { stdout } = await execFileAsync("git", args, {
16178
+ cwd,
16179
+ timeout: GIT_TIMEOUT_MS,
16180
+ maxBuffer: GIT_MAX_BUFFER
16181
+ });
16182
+ return stdout.trim();
16183
+ }
16184
+ async function safeGitOutput(cwd, args) {
16185
+ try {
16186
+ return await gitOutput(cwd, args);
16187
+ } catch {
16188
+ return null;
16189
+ }
16190
+ }
16191
+ async function readMarkdownFiles(root) {
16192
+ const paths = (await walkMarkdown(root, root)).slice(0, MAX_MARKDOWN_FILES);
16193
+ return (await Promise.all(paths.map(async (path) => {
16194
+ const absolutePath = join(root, path);
16195
+ if ((await stat(absolutePath)).size > MAX_MARKDOWN_FILE_BYTES) return null;
16196
+ return {
16197
+ relativePath: path,
16198
+ parsed: parseMarkdown(await readFile(absolutePath, "utf8"))
16199
+ };
16200
+ }))).filter((file) => file !== null);
16201
+ }
16202
+ function parseMarkdown(raw) {
16203
+ try {
16204
+ const parsed = matter(raw);
16205
+ return {
16206
+ content: parsed.content,
16207
+ data: parsed.data
16208
+ };
16209
+ } catch {
16210
+ return parseMarkdownFallback(raw);
16211
+ }
16212
+ }
16213
+ function parseMarkdownFallback(raw) {
16214
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(raw);
16215
+ if (!match) return {
16216
+ content: raw,
16217
+ data: {}
16218
+ };
16219
+ const frontmatter = match[1] ?? "";
16220
+ const data = {};
16221
+ for (const line of frontmatter.split(/\r?\n/)) {
16222
+ const field = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line.trim());
16223
+ if (!field) continue;
16224
+ const key = field[1];
16225
+ const value = field[2] ?? "";
16226
+ if (key === "title") {
16227
+ data.title = value.replace(/^["']|["']$/g, "");
16228
+ continue;
16229
+ }
16230
+ if (key === "owners" || key === "soft_links") data[key] = parseInlineStringList(value);
16231
+ }
16232
+ return {
16233
+ content: raw.slice(match[0].length),
16234
+ data
16235
+ };
16236
+ }
16237
+ function parseInlineStringList(value) {
16238
+ const trimmed = value.trim();
16239
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return [];
16240
+ return trimmed.slice(1, -1).split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter((item) => item.length > 0);
16241
+ }
16242
+ async function walkMarkdown(root, current) {
16243
+ const entries = await readdir(current, { withFileTypes: true });
16244
+ const paths = [];
16245
+ for (const entry of entries) {
16246
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
16247
+ const absolute = join(current, entry.name);
16248
+ if (entry.isDirectory()) {
16249
+ paths.push(...await walkMarkdown(root, absolute));
16250
+ continue;
16251
+ }
16252
+ if (entry.isFile() && extname(entry.name).toLowerCase() === ".md") paths.push(toPosix(relative(root, absolute)));
16253
+ }
16254
+ paths.sort((a, b) => a.localeCompare(b));
16255
+ return paths;
16256
+ }
16257
+ function buildTree(files) {
16258
+ const nodeBySourcePath = /* @__PURE__ */ new Map();
16259
+ const nodeByTreePath = /* @__PURE__ */ new Map();
16260
+ const dirNodeContent = /* @__PURE__ */ new Map();
16261
+ const leafFiles = [];
16262
+ const directories = new Set([""]);
16263
+ for (const file of files) {
16264
+ const dir = sourceDir(file.relativePath);
16265
+ addDirectoryAncestors(directories, dir);
16266
+ if (file.relativePath.endsWith(`/${NODE_FILE}`) || file.relativePath === NODE_FILE) dirNodeContent.set(dir, file);
16267
+ else leafFiles.push(file);
16268
+ }
16269
+ const rootNode = {
16270
+ id: ROOT_NODE_ID,
16271
+ path: "",
16272
+ sourcePath: NODE_FILE,
16273
+ title: titleFromFile(dirNodeContent.get("")?.parsed.data, "Context Tree"),
16274
+ kind: "root",
16275
+ owners: ownersFromFile(dirNodeContent.get("")?.parsed.data),
16276
+ parentId: null,
16277
+ preview: previewFromContent(dirNodeContent.get("")?.parsed.content ?? ""),
16278
+ relatedNodeIds: [],
16279
+ affectedContextArea: "root",
16280
+ changeType: null,
16281
+ changedAtCommit: null
16282
+ };
16283
+ const nodes = [rootNode];
16284
+ nodeByTreePath.set("", rootNode);
16285
+ if (dirNodeContent.has("")) nodeBySourcePath.set(NODE_FILE, rootNode);
16286
+ const sortedDirs = [...directories].filter((dir) => dir.length > 0).sort((a, b) => a.localeCompare(b));
16287
+ for (const dir of sortedDirs) {
16288
+ const source = dirNodeContent.get(dir);
16289
+ const node = {
16290
+ id: dirNodeId(dir),
16291
+ path: dir,
16292
+ sourcePath: source?.relativePath ?? null,
16293
+ title: titleFromFile(source?.parsed.data, titleFromPath(dir)),
16294
+ kind: kindForDirectory(dir),
16295
+ owners: ownersFromFile(source?.parsed.data),
16296
+ parentId: parentDir(dir) ? dirNodeId(parentDir(dir)) : ROOT_NODE_ID,
16297
+ preview: previewFromContent(source?.parsed.content ?? ""),
16298
+ relatedNodeIds: [],
16299
+ affectedContextArea: contextAreaFromPath(dir),
16300
+ changeType: null,
16301
+ changedAtCommit: null
16302
+ };
16303
+ nodes.push(node);
16304
+ nodeByTreePath.set(dir, node);
16305
+ if (source) nodeBySourcePath.set(source.relativePath, node);
16306
+ }
16307
+ for (const file of leafFiles) {
16308
+ const treePath = stripMarkdownExtension(file.relativePath);
16309
+ const dir = sourceDir(file.relativePath);
16310
+ const node = {
16311
+ id: fileNodeId(file.relativePath),
16312
+ path: treePath,
16313
+ sourcePath: file.relativePath,
16314
+ title: titleFromFile(file.parsed.data, titleFromPath(treePath)),
16315
+ kind: "leaf",
16316
+ owners: ownersFromFile(file.parsed.data),
16317
+ parentId: dir ? dirNodeId(dir) : ROOT_NODE_ID,
16318
+ preview: previewFromContent(file.parsed.content),
16319
+ relatedNodeIds: [],
16320
+ affectedContextArea: contextAreaFromPath(treePath),
16321
+ changeType: null,
16322
+ changedAtCommit: null
16323
+ };
16324
+ nodes.push(node);
16325
+ nodeByTreePath.set(treePath, node);
16326
+ nodeBySourcePath.set(file.relativePath, node);
16327
+ }
16328
+ const edges = parentEdges(nodes);
16329
+ const relatedEdges = relatedEdgesForFiles(files, nodeByTreePath, nodeBySourcePath);
16330
+ const relatedByNode = /* @__PURE__ */ new Map();
16331
+ for (const edge of relatedEdges) {
16332
+ if (!relatedByNode.has(edge.source)) relatedByNode.set(edge.source, /* @__PURE__ */ new Set());
16333
+ relatedByNode.get(edge.source)?.add(edge.target);
16334
+ }
16335
+ return {
16336
+ nodes: nodes.map((node) => ({
16337
+ ...node,
16338
+ relatedNodeIds: [...relatedByNode.get(node.id) ?? /* @__PURE__ */ new Set()]
16339
+ })),
16340
+ edges: [...edges, ...relatedEdges],
16341
+ nodeBySourcePath,
16342
+ nodeByTreePath
16343
+ };
16344
+ }
16345
+ function parentEdges(nodes) {
16346
+ return nodes.filter((node) => node.parentId).map((node) => ({
16347
+ source: node.parentId ?? ROOT_NODE_ID,
16348
+ target: node.id,
16349
+ kind: "parent"
16350
+ }));
16351
+ }
16352
+ function relatedEdgesForFiles(files, nodeByTreePath, nodeBySourcePath) {
16353
+ const edges = [];
16354
+ const seen = /* @__PURE__ */ new Set();
16355
+ for (const file of files) {
16356
+ const sourceNode = nodeBySourcePath.get(file.relativePath);
16357
+ if (!sourceNode) continue;
16358
+ const softLinks = stringArrayField(file.parsed.data, "soft_links");
16359
+ for (const link of softLinks) {
16360
+ const target = resolveLinkedNode(link, file.relativePath, nodeByTreePath);
16361
+ if (!target) continue;
16362
+ const key = `${sourceNode.id}:soft_link:${target.id}`;
16363
+ if (seen.has(key)) continue;
16364
+ seen.add(key);
16365
+ edges.push({
16366
+ source: sourceNode.id,
16367
+ target: target.id,
16368
+ kind: "soft_link"
16369
+ });
16370
+ }
16371
+ for (const link of markdownLinks(file.parsed.content)) {
16372
+ const target = resolveLinkedNode(link, file.relativePath, nodeByTreePath);
16373
+ if (!target) continue;
16374
+ const key = `${sourceNode.id}:markdown_link:${target.id}`;
16375
+ if (seen.has(key)) continue;
16376
+ seen.add(key);
16377
+ edges.push({
16378
+ source: sourceNode.id,
16379
+ target: target.id,
16380
+ kind: "markdown_link"
16381
+ });
16382
+ }
16383
+ }
16384
+ return edges;
16385
+ }
16386
+ async function comparisonBaseForWindow(root, window) {
16387
+ const commitBeforeWindow = await safeGitOutput(root, [
16388
+ "rev-list",
16389
+ "-1",
16390
+ `--before=${(/* @__PURE__ */ new Date(Date.now() - WINDOW_DAYS[window] * 24 * 60 * 60 * 1e3)).toISOString()}`,
16391
+ "HEAD"
16392
+ ]);
16393
+ return commitBeforeWindow && commitBeforeWindow.length > 0 ? commitBeforeWindow : EMPTY_TREE_COMMIT;
16394
+ }
16395
+ async function readDiffEntries(root, comparisonBase, headCommit) {
16396
+ if (!isSafeCommit(comparisonBase) || !isSafeCommit(headCommit)) return {
16397
+ entries: [],
16398
+ truncated: false
16399
+ };
16400
+ try {
16401
+ const output = await gitOutput(root, [
16402
+ "diff",
16403
+ "--name-status",
16404
+ "-M",
16405
+ comparisonBase,
16406
+ "HEAD",
16407
+ "--",
16408
+ "*.md"
16409
+ ]);
16410
+ if (!output) return {
16411
+ entries: [],
16412
+ truncated: false
16413
+ };
16414
+ const pendingEntries = [];
16415
+ for (const line of output.split("\n")) {
16416
+ if (pendingEntries.length >= MAX_DIFF_ENTRIES) break;
16417
+ const parts = line.split(" ").filter(Boolean);
16418
+ const status = parts[0];
16419
+ if (!status) continue;
16420
+ if (status.startsWith("R")) {
16421
+ const oldPath = parts[1];
16422
+ const newPath = parts[2];
16423
+ if (oldPath) pendingEntries.push({
16424
+ type: "removed",
16425
+ path: toPosix(oldPath)
16426
+ });
16427
+ if (pendingEntries.length >= MAX_DIFF_ENTRIES) break;
16428
+ if (newPath) pendingEntries.push({
16429
+ type: "added",
16430
+ path: toPosix(newPath)
16431
+ });
16432
+ continue;
16433
+ }
16434
+ const path = parts[1];
16435
+ if (!path) continue;
16436
+ if (status === "A") pendingEntries.push({
16437
+ type: "added",
16438
+ path: toPosix(path)
16439
+ });
16440
+ if (status === "M") pendingEntries.push({
16441
+ type: "edited",
16442
+ path: toPosix(path)
16443
+ });
16444
+ if (status === "D") pendingEntries.push({
16445
+ type: "removed",
16446
+ path: toPosix(path)
16447
+ });
16448
+ }
16449
+ const metadataByPath = await readChangeMetadataByPath(root, comparisonBase, headCommit, pendingEntries.map((entry) => entry.path));
16450
+ const entries = pendingEntries.map((entry) => ({
16451
+ ...entry,
16452
+ ...metadataByPath.get(entry.path) ?? fallbackChangeMetadata(headCommit)
16453
+ }));
16454
+ return {
16455
+ entries,
16456
+ truncated: output.split("\n").filter(Boolean).length > entries.length
16457
+ };
16458
+ } catch {
16459
+ return {
16460
+ entries: [],
16461
+ truncated: false
16462
+ };
16463
+ }
16464
+ }
16465
+ async function readChangeMetadataByPath(root, comparisonBase, headCommit, paths) {
16466
+ const uniquePaths = [...new Set(paths)];
16467
+ const metadataByPath = /* @__PURE__ */ new Map();
16468
+ if (uniquePaths.length === 0) return metadataByPath;
16469
+ const output = await safeGitOutput(root, [
16470
+ "log",
16471
+ "--name-only",
16472
+ `--format=${GIT_LOG_RECORD_SEPARATOR}%H%x00%cI%x00%an%x00%s`,
16473
+ `${comparisonBase}..HEAD`,
16474
+ "--",
16475
+ ...uniquePaths
16476
+ ]);
16477
+ if (!output) return metadataByPath;
16478
+ for (const rawRecord of output.split(GIT_LOG_RECORD_SEPARATOR)) {
16479
+ const record = rawRecord.trim();
16480
+ if (!record) continue;
16481
+ const newlineIndex = record.indexOf("\n");
16482
+ const header = newlineIndex === -1 ? record : record.slice(0, newlineIndex);
16483
+ const changedPaths = newlineIndex === -1 ? [] : record.slice(newlineIndex + 1).split("\n").map((path) => toPosix(path.trim())).filter((path) => path.length > 0);
16484
+ const fields = header.split("\0");
16485
+ const commit = fields[0];
16486
+ if (!commit || !isSafeCommit(commit)) continue;
16487
+ const metadata = {
16488
+ commit,
16489
+ changedAt: fields[1] && fields[1].length > 0 ? fields[1] : null,
16490
+ changedBy: fields[2] && fields[2].length > 0 ? fields[2] : null,
16491
+ summary: cleanCommitSubject(fields[3] ?? null)
16492
+ };
16493
+ for (const changedPath of changedPaths) if (!metadataByPath.has(changedPath)) metadataByPath.set(changedPath, metadata);
16494
+ }
16495
+ for (const path of uniquePaths) if (!metadataByPath.has(path)) metadataByPath.set(path, fallbackChangeMetadata(headCommit));
16496
+ return metadataByPath;
16497
+ }
16498
+ function fallbackChangeMetadata(headCommit) {
16499
+ return {
16500
+ commit: headCommit,
16501
+ changedAt: null,
16502
+ changedBy: null,
16503
+ summary: null
16504
+ };
16505
+ }
16506
+ function isSafeCommit(value) {
16507
+ return /^[0-9a-f]{40}$/i.test(value);
16508
+ }
16509
+ function cleanCommitSubject(subject) {
16510
+ if (!subject) return null;
16511
+ const cleaned = subject.trim().replace(/^(feat|fix|docs|chore|refactor|test|style|perf|ci|build)(\([^)]+\))?:\s*/i, "").replace(/\s+/g, " ");
16512
+ if (cleaned.length < 12) return null;
16513
+ if (cleaned.length > 140) return `${cleaned.slice(0, 137)}...`;
16514
+ if (/^(merge|wip|update|updated|change|changes)$/i.test(cleaned)) return null;
16515
+ return cleaned;
16516
+ }
16517
+ function buildChanges(entries, tree) {
16518
+ return entries.map((entry) => {
16519
+ const node = tree.nodeBySourcePath.get(entry.path) ?? tree.nodeByTreePath.get(stripMarkdownExtension(entry.path));
16520
+ return {
16521
+ path: entry.path,
16522
+ nodeId: node?.id ?? ghostNodeId(entry.path),
16523
+ type: entry.type,
16524
+ commit: entry.commit,
16525
+ changedAt: entry.changedAt,
16526
+ changedBy: entry.changedBy,
16527
+ summary: entry.summary
16528
+ };
16529
+ });
16530
+ }
16531
+ function applyChangesToNodes(nodes, changes) {
16532
+ const changeByNode = /* @__PURE__ */ new Map();
16533
+ for (const change of changes) if (change.nodeId) changeByNode.set(change.nodeId, change);
16534
+ return nodes.map((node) => {
16535
+ const change = changeByNode.get(node.id);
16536
+ if (!change) return node;
16537
+ return {
16538
+ ...node,
16539
+ changeType: change.type,
16540
+ changedAtCommit: change.commit
16541
+ };
16542
+ });
16543
+ }
16544
+ function addRemovedGhostNodes(nodes, changes) {
16545
+ const nodeIds = new Set(nodes.map((node) => node.id));
16546
+ const ghosts = [];
16547
+ for (const change of changes) {
16548
+ if (change.type !== "removed" || !change.nodeId || nodeIds.has(change.nodeId)) continue;
16549
+ const treePath = stripMarkdownExtension(change.path);
16550
+ const dir = sourceDir(change.path);
16551
+ const parentNodeId = dir ? dirNodeId(dir) : ROOT_NODE_ID;
16552
+ ghosts.push({
16553
+ id: change.nodeId,
16554
+ path: treePath,
16555
+ sourcePath: change.path,
16556
+ title: titleFromPath(treePath),
16557
+ kind: "leaf",
16558
+ owners: [],
16559
+ parentId: nodeIds.has(parentNodeId) ? parentNodeId : ROOT_NODE_ID,
16560
+ preview: null,
16561
+ relatedNodeIds: [],
16562
+ affectedContextArea: contextAreaFromPath(treePath),
16563
+ changeType: "removed",
16564
+ changedAtCommit: change.commit
16565
+ });
16566
+ }
16567
+ return [...nodes, ...ghosts];
16568
+ }
16569
+ function summarizeChanges(changes) {
16570
+ let addedCount = 0;
16571
+ let editedCount = 0;
16572
+ let removedCount = 0;
16573
+ for (const change of changes) {
16574
+ if (change.type === "added") addedCount += 1;
16575
+ if (change.type === "edited") editedCount += 1;
16576
+ if (change.type === "removed") removedCount += 1;
16577
+ }
16578
+ return {
16579
+ addedCount,
16580
+ editedCount,
16581
+ removedCount,
16582
+ changedNodeCount: changes.length
16583
+ };
16584
+ }
16585
+ function buildUpdates(changes, nodes) {
16586
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
16587
+ const updates = changes.map((change) => {
16588
+ const node = change.nodeId ? nodeById.get(change.nodeId) : void 0;
16589
+ const path = node?.path ?? stripMarkdownExtension(change.path);
16590
+ const title = node?.title ?? titleFromPath(path);
16591
+ const affectedContextArea = node?.affectedContextArea ?? contextAreaFromPath(path);
16592
+ return {
16593
+ id: `update:${change.type}:${change.path}`,
16594
+ nodeId: change.nodeId,
16595
+ path,
16596
+ title,
16597
+ changeType: change.type,
16598
+ affectedContextArea,
16599
+ reason: reasonForUpdate(change.type, affectedContextArea),
16600
+ summary: changeSummaryForUpdate(change, node),
16601
+ changedBy: change.changedBy,
16602
+ owners: node?.owners ?? [],
16603
+ relatedNodeIds: node?.relatedNodeIds ?? [],
16604
+ sourceCommit: change.commit,
16605
+ riskLevel: riskLevelForChange(change.type, node?.kind)
16606
+ };
16607
+ });
16608
+ updates.sort((a, b) => updateRank(a) - updateRank(b) || a.path.localeCompare(b.path));
16609
+ return updates;
16610
+ }
16611
+ function changeSummaryForUpdate(change, node) {
16612
+ if (change.summary) return change.summary;
16613
+ if (change.type === "added") return "added this team knowledge";
16614
+ if (change.type === "removed") return "removed this team knowledge";
16615
+ return node ? `updated ${node.title}` : "updated this team knowledge";
16616
+ }
16617
+ function updateRank(update) {
16618
+ const depth = update.path.split("/").filter(Boolean).length;
16619
+ const specificityRank = depth >= 2 ? 0 : depth === 1 ? 1 : 2;
16620
+ const typeRank = update.changeType === "removed" ? 0 : update.changeType === "added" ? 1 : 2;
16621
+ return specificityRank * 10 + typeRank;
16622
+ }
16623
+ function riskLevelForChange(changeType, kind) {
16624
+ if (changeType === "removed") return "high";
16625
+ if (kind === "root" || kind === "domain" || kind === "subdomain") return "medium";
16626
+ return "low";
16627
+ }
16628
+ function reasonForUpdate(changeType, affectedContextArea) {
16629
+ if (changeType === "added") return `Agents can use new team knowledge when working on ${affectedContextArea}.`;
16630
+ if (changeType === "removed") return `Agents should stop using the old team knowledge for ${affectedContextArea}.`;
16631
+ return `Agents can use updated team knowledge when working on ${affectedContextArea}.`;
16632
+ }
16633
+ function titleFromFile(data, fallback) {
16634
+ return stringField(data, "title") ?? fallback;
16635
+ }
16636
+ function ownersFromFile(data) {
16637
+ return stringArrayField(data, "owners");
16638
+ }
16639
+ function stringField(data, key) {
16640
+ if (!isRecord$1(data)) return null;
16641
+ const value = data[key];
16642
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
16643
+ }
16644
+ function stringArrayField(data, key) {
16645
+ if (!isRecord$1(data)) return [];
16646
+ const value = data[key];
16647
+ if (!Array.isArray(value)) return [];
16648
+ return value.filter((item) => typeof item === "string" && item.trim().length > 0);
16649
+ }
16650
+ function isRecord$1(value) {
16651
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16652
+ }
16653
+ function previewFromContent(content) {
16654
+ const normalized = content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).join(" ");
16655
+ if (!normalized) return null;
16656
+ return normalized.length > 240 ? `${normalized.slice(0, 237)}...` : normalized;
16657
+ }
16658
+ function markdownLinks(content) {
16659
+ const links = [];
16660
+ const re = /\[[^\]]+\]\(([^)]+)\)/g;
16661
+ let match = re.exec(content);
16662
+ while (match) {
16663
+ const target = match[1];
16664
+ if (target && !/^https?:\/\//.test(target) && !target.startsWith("#")) links.push(target.split("#")[0] ?? target);
16665
+ match = re.exec(content);
16666
+ }
16667
+ return links;
16668
+ }
16669
+ function resolveLinkedNode(link, fromSourcePath, nodeByTreePath) {
16670
+ const withoutAnchor = link.split("#")[0] ?? link;
16671
+ if (!withoutAnchor) return null;
16672
+ const baseDir = sourceDir(fromSourcePath);
16673
+ const cleaned = (withoutAnchor.startsWith("/") ? withoutAnchor.slice(1) : toPosix(normalize(join(baseDir, withoutAnchor)))).replace(/^\.\//, "");
16674
+ const candidates = [
16675
+ stripMarkdownExtension(cleaned),
16676
+ stripMarkdownExtension(cleaned.replace(/\/NODE\.md$/i, "")),
16677
+ cleaned.replace(/\/$/g, "")
16678
+ ].filter((candidate) => candidate.length > 0);
16679
+ for (const candidate of candidates) {
16680
+ const node = nodeByTreePath.get(candidate);
16681
+ if (node) return node;
16682
+ }
16683
+ return null;
16684
+ }
16685
+ function addDirectoryAncestors(directories, dir) {
16686
+ if (!dir) return;
16687
+ const parts = dir.split("/");
16688
+ for (let i = 1; i <= parts.length; i += 1) directories.add(parts.slice(0, i).join("/"));
16689
+ }
16690
+ function kindForDirectory(dir) {
16691
+ return dir.includes("/") ? "subdomain" : "domain";
16692
+ }
16693
+ function sourceDir(path) {
16694
+ const dir = toPosix(dirname(path));
16695
+ return dir === "." ? "" : dir;
16696
+ }
16697
+ function parentDir(dir) {
16698
+ const parent = toPosix(dirname(dir));
16699
+ return parent === "." ? "" : parent;
16700
+ }
16701
+ function stripMarkdownExtension(path) {
16702
+ return path.replace(/\/NODE\.md$/i, "").replace(/\.md$/i, "");
16703
+ }
16704
+ function titleFromPath(path) {
16705
+ return (path.split("/").filter(Boolean).at(-1) ?? "Context Tree").replace(/[-_]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()).trim();
16706
+ }
16707
+ function contextAreaFromPath(path) {
16708
+ const parts = path.split("/").filter(Boolean);
16709
+ if (parts.length === 0) return "root";
16710
+ return parts.map((part) => part.replace(/[-_]+/g, " ")).join(" / ");
16711
+ }
16712
+ function dirNodeId(dir) {
16713
+ return dir ? `dir:${dir}` : ROOT_NODE_ID;
16714
+ }
16715
+ function fileNodeId(path) {
16716
+ return `file:${path}`;
16717
+ }
16718
+ function ghostNodeId(path) {
16719
+ return `removed:${path}`;
16720
+ }
16721
+ function toPosix(path) {
16722
+ return sep === "/" ? path : path.split(sep).join("/");
16723
+ }
16724
+ const querySchema = z.object({ window: z.enum([
16725
+ "1d",
16726
+ "7d",
16727
+ "30d"
16728
+ ]).optional() }).strict();
16729
+ async function contextTreeSnapshotRoutes(app) {
16730
+ app.get("/snapshot", { config: { rateLimit: {
16731
+ max: app.config.rateLimit?.contextTreeSnapshotMax ?? 6,
16732
+ timeWindow: "1 minute",
16733
+ keyGenerator: (request) => request.user?.userId ?? request.ip
16734
+ } } }, async (request) => {
16735
+ const query = querySchema.parse(request.query);
16736
+ const snapshot = await getContextTreeSnapshot(app.config, query.window ?? "7d");
16737
+ return contextTreeSnapshotSchema.parse(snapshot);
16738
+ });
16739
+ }
15937
16740
  /**
15938
16741
  * Resolve the client IP for rate-limit attribution.
15939
16742
  *
@@ -16035,7 +16838,7 @@ async function healthzRoutes(app) {
16035
16838
  * `api/orgs/invitations.ts` (Class B, admin-gated).
16036
16839
  */
16037
16840
  async function publicInvitationRoutes(app) {
16038
- const { previewInvitation } = await import("./invitation-DWlyNb8x-DZTW9I26.mjs");
16841
+ const { previewInvitation } = await import("./invitation-DWlyNb8x-BEgoZ9k1.mjs");
16039
16842
  app.get("/:token/preview", async (request, reply) => {
16040
16843
  if (!request.params.token) throw new UnauthorizedError("Token required");
16041
16844
  const preview = await previewInvitation(app.db, request.params.token);
@@ -16132,9 +16935,35 @@ async function meRoutes(app) {
16132
16935
  */
16133
16936
  app.get("/me/pinned-agents", async (request) => {
16134
16937
  const { userId } = requireUser(request);
16135
- const { listMyPinnedAgents } = await import("./client-m1OM4Iag-HKWgB3Yk.mjs");
16938
+ const { listMyPinnedAgents } = await import("./client-By1K4VVT-nVOhsXBy.mjs");
16136
16939
  return listMyPinnedAgents(app.db, { userId });
16137
16940
  });
16941
+ /**
16942
+ * GET /me/clients — cross-org list of every client owned by the caller.
16943
+ * A client is owned by exactly one user (clients.user_id) and the same
16944
+ * machine can carry agents from any org the user belongs to, so this
16945
+ * surface is org-agnostic — Class A by the decision tree in
16946
+ * `docs/http-path-conventions.md`. Powers Settings → Computers in the
16947
+ * web UI; the org-admin audit view (`/orgs/:orgId/clients`) stays for
16948
+ * a future "team device audit" surface.
16949
+ */
16950
+ app.get("/me/clients", async (request) => {
16951
+ const { userId } = requireUser(request);
16952
+ const list = await listClients(app.db, { userId });
16953
+ const refreshExpirySeconds = expiryToSeconds(app.config.auth.refreshTokenExpiry);
16954
+ return list.map((c) => ({
16955
+ id: c.id,
16956
+ userId: c.userId,
16957
+ status: c.status,
16958
+ authState: deriveAuthState(c, refreshExpirySeconds),
16959
+ sdkVersion: c.sdkVersion,
16960
+ hostname: c.hostname,
16961
+ os: c.os,
16962
+ agentCount: c.agentCount,
16963
+ connectedAt: serializeDate(c.connectedAt),
16964
+ lastSeenAt: c.lastSeenAt.toISOString()
16965
+ }));
16966
+ });
16138
16967
  app.get("/me/organizations", async (request) => {
16139
16968
  const { userId } = requireUser(request);
16140
16969
  return (await listActiveMemberships(app.db, userId)).map((r) => ({
@@ -19088,6 +19917,9 @@ async function buildApp(config) {
19088
19917
  await api.register(publicInvitationRoutes, { prefix: "/invitations" });
19089
19918
  await api.register(contextTreeInfoRoutes, { prefix: "/context-tree" });
19090
19919
  await api.register(bootstrapConfigRoutes, { prefix: "/bootstrap" });
19920
+ await api.register(userScope("contextTreeScope", async (scope) => {
19921
+ await scope.register(contextTreeSnapshotRoutes);
19922
+ }), { prefix: "/context-tree" });
19091
19923
  await api.register(userScope("meRoutesScope", async (scope) => {
19092
19924
  await scope.register(meRoutes);
19093
19925
  }), { prefix: "" });
@@ -19151,12 +19983,11 @@ async function buildApp(config) {
19151
19983
  if (webDistPath) {
19152
19984
  const webRoot = resolve(webDistPath);
19153
19985
  if (existsSync(webRoot)) {
19154
- await app.register(fastifyStatic, {
19155
- root: webRoot,
19156
- wildcard: false
19157
- });
19986
+ await app.register(fastifyStatic, { root: webRoot });
19158
19987
  app.setNotFoundHandler((request, reply) => {
19159
19988
  if (request.url.startsWith("/api/")) return reply.status(404).send({ error: "Not found" });
19989
+ const requestPath = request.url.split("?")[0] ?? request.url;
19990
+ if (requestPath.startsWith("/assets/") || extname(requestPath).length > 0) return reply.status(404).send({ error: "Not found" });
19160
19991
  return reply.sendFile("index.html");
19161
19992
  });
19162
19993
  }
@@ -19317,7 +20148,7 @@ async function startServer(options) {
19317
20148
  instanceId: `srv_${randomUUID().slice(0, 8)}`,
19318
20149
  commandVersion: COMMAND_VERSION
19319
20150
  };
19320
- const { initTelemetry, shutdownTelemetry } = await import("./observability-Co8OO0og.mjs");
20151
+ const { initTelemetry, shutdownTelemetry } = await import("./observability-DttujCqj.mjs");
19321
20152
  await initTelemetry(serverConfig.observability.tracing, config.instanceId);
19322
20153
  const app = await buildApp(config);
19323
20154
  const SHUTDOWN_FORCE_EXIT_MS = 8e3;