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