@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.
- package/dist/{bootstrap-TJRy0B9m.mjs → bootstrap-B-FRMuvL.mjs} +3 -1
- package/dist/cli/index.mjs +8 -8
- package/dist/client-By1K4VVT-nVOhsXBy.mjs +4 -0
- package/dist/{client-BCaK653p-CZjDNcdM.mjs → client-CLdRbuml-B416INrm.mjs} +11 -3
- package/dist/{dist-BkvrONSQ.mjs → dist-FuUBFTEB.mjs} +99 -1
- package/dist/{feishu-AEMHwT6L.mjs → feishu-GvFABWW5.mjs} +1 -1
- package/dist/index.mjs +6 -6
- package/dist/{invitation-DWlyNb8x-DZTW9I26.mjs → invitation-DWlyNb8x-BEgoZ9k1.mjs} +1 -1
- package/dist/{observability-C3nY6Jcz-Dpsi3eFk.mjs → observability-C3nY6Jcz-Bk7FX689.mjs} +1 -1
- package/dist/{observability-Co8OO0og.mjs → observability-DttujCqj.mjs} +1 -1
- package/dist/{saas-connect-Bd0g0v_b.mjs → saas-connect-Df2CVAGp.mjs} +850 -19
- package/dist/web/assets/index-43trJLR8.js +388 -0
- package/dist/web/assets/{index-Dbwa40_B.js → index-CD7rTdqm.js} +1 -1
- package/dist/web/assets/index-fNb_M0nL.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/client-m1OM4Iag-HKWgB3Yk.mjs +0 -4
- package/dist/web/assets/index-7RvlJjJ9.css +0 -1
- package/dist/web/assets/index-cpdSFHAJ.js +0 -383
- package/dist/{src-uVZSbShB.mjs → src-CzQ5KF6D.mjs} +1 -1
|
@@ -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-
|
|
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-
|
|
4
|
-
import { $ as
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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;
|