@agent-team-foundation/first-tree-hub 0.14.6 → 0.14.7-alpha.281.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { $ as SdkError, A as printResults, B as ClientRuntime, C as migrateLocalAgentDirs, D as checkNodeVersion, E as checkClientConfig, G as removeLocalAgent, I as restartClientService, J as fail, K as resolveReplyToFromEnv, L as startClientService, M as getClientServiceStatus, N as installClientService, O as checkServerReachable, P as isServiceSupported, Q as FirstTreeHubSDK, R as stopClientService, S as createApiNameResolver, T as checkBackgroundService, U as findStaleAliases, V as handleClientOrgMismatch, W as formatStaleReason, X as ClientOrgMismatchError, Y as success, Z as ClientUserMismatchError, _ as loadOnboardState, a as declineUpdate, b as saveOnboardState, c as detectInstallMode, d as reconcileLocalRuntimeProviders, et as SessionRegistry, f as uploadClientCapabilities, g as formatCheckReport, h as promptMissingFields, i as createExecuteUpdate, it as configureClientLoggerForService, j as reconcileAgentConfigs, k as checkWebSocket, l as fetchLatestVersion, m as promptAddAgent, nt as probeCapabilities, o as promptUpdate, p as isInteractive, q as resolveSenderName, r as registerSaaSConnectCommand, rt as applyClientLoggerConfig, s as PACKAGE_NAME, tt as cleanWorkspaces, u as installGlobalLatest, v as onboardCheck, w as checkAgentConfigs, x as runHomeMigration, y as onboardCreate } from "../saas-connect-CNY9Ve5V.mjs";
2
+ import { $ as SdkError, A as printResults, B as ClientRuntime, C as migrateLocalAgentDirs, D as checkNodeVersion, E as checkClientConfig, G as removeLocalAgent, I as restartClientService, J as fail, K as resolveReplyToFromEnv, L as startClientService, M as getClientServiceStatus, N as installClientService, O as checkServerReachable, P as isServiceSupported, Q as FirstTreeHubSDK, R as stopClientService, S as createApiNameResolver, T as checkBackgroundService, U as findStaleAliases, V as handleClientOrgMismatch, W as formatStaleReason, X as ClientOrgMismatchError, Y as success, Z as ClientUserMismatchError, _ as loadOnboardState, a as declineUpdate, b as saveOnboardState, c as detectInstallMode, d as reconcileLocalRuntimeProviders, et as SessionRegistry, f as uploadClientCapabilities, g as formatCheckReport, h as promptMissingFields, i as createExecuteUpdate, it as configureClientLoggerForService, j as reconcileAgentConfigs, k as checkWebSocket, l as fetchLatestVersion, m as promptAddAgent, nt as probeCapabilities, o as promptUpdate, p as isInteractive, q as resolveSenderName, r as registerSaaSConnectCommand, rt as applyClientLoggerConfig, s as PACKAGE_NAME, tt as cleanWorkspaces, u as installGlobalLatest, v as onboardCheck, w as checkAgentConfigs, x as runHomeMigration, y as onboardCreate } from "../saas-connect-gXv14p6J.mjs";
3
3
  import { C as resolveConfigReadonly, S as resetConfigMeta, _ as initConfig, a as loadCredentials, b as readConfigFile, c as saveAgentConfig, d as DEFAULT_DATA_DIR, f as DEFAULT_HOME_DIR, g as getConfigValue, i as ensureFreshAdminToken, m as clientConfigSchema, p as agentConfigSchema, r as ensureFreshAccessToken, s as resolveServerUrl, u as DEFAULT_CONFIG_DIR, v as loadAgents, w as setConfigValue, x as resetConfig } from "../bootstrap-CmkHQsnS.mjs";
4
4
  import { a as print, n as CLI_USER_AGENT, o as setJsonMode, r as COMMAND_VERSION, t as cliFetch } from "../cli-fetch-BGVItZxo.mjs";
5
- import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-BE7QRxnE.mjs";
5
+ import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-BBUEVTnb.mjs";
6
6
  import { join } from "node:path";
7
7
  import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
8
8
  import * as semver from "semver";
@@ -545,10 +545,7 @@ const chatMetadataSchema = z.discriminatedUnion("source", [githubChatMetadataSch
545
545
  const optionalChatMetadataSchema = z.union([z.object({}).strict(), chatMetadataSchema]);
546
546
  const chatSourceSchema = z.enum([
547
547
  "manual",
548
- "github_issue",
549
- "github_pull_request",
550
- "github_discussion",
551
- "github_commit",
548
+ "github",
552
549
  "feishu"
553
550
  ]);
554
551
  const chatTypeSchema = z.enum(["direct", "group"]);
@@ -577,7 +574,9 @@ const chatParticipantDetailSchema = z.object({
577
574
  }).extend({
578
575
  name: z.string().nullable(),
579
576
  displayName: z.string(),
580
- type: z.string()
577
+ type: z.string(),
578
+ avatarColorToken: z.string().nullable(),
579
+ avatarImageUrl: z.string().nullable()
581
580
  });
582
581
  z.object({
583
582
  id: z.string(),
@@ -598,6 +597,47 @@ z.object({
598
597
  z.object({ topic: z.string().trim().max(500).nullable() });
599
598
  z.object({ agentId: z.string().min(1) });
600
599
  z.object({ agentId: z.string().min(1) });
600
+ /**
601
+ * Schema for `GET /api/v1/chats/:chatId/github-entities`.
602
+ *
603
+ * Each item describes one entity bound to the chat via
604
+ * `github_entity_chat_mappings`. Live state (`title`, `state`) is fetched
605
+ * per-request from the GitHub REST API at request time and NEVER
606
+ * persisted to the database — the user explicitly opted for the
607
+ * fresh-on-every-open model rather than a webhook-synced column.
608
+ *
609
+ * Live fields may be `null` when the GitHub call failed (token unavailable,
610
+ * rate-limited, 404 on a deleted entity, etc.). The client falls back to
611
+ * rendering `entityKey + htmlUrl` so the row is still a working link.
612
+ */
613
+ const githubEntityBoundViaSchema = z.enum([
614
+ "direct",
615
+ "fixes_link",
616
+ "agent_created"
617
+ ]);
618
+ /**
619
+ * Coarse live state. PR-specific values (`merged`, `draft`) are folded
620
+ * into the same enum as the issue's `open` / `closed` so the UI can
621
+ * switch on a single field. `null` means we couldn't reach GitHub for
622
+ * this entity in this request — the row still renders, just without
623
+ * the state badge.
624
+ */
625
+ const githubEntityLiveStateSchema = z.enum([
626
+ "open",
627
+ "closed",
628
+ "merged",
629
+ "draft"
630
+ ]);
631
+ const chatGithubEntitySchema = z.object({
632
+ entityType: githubEntityTypeSchema,
633
+ entityKey: z.string().min(1),
634
+ boundVia: githubEntityBoundViaSchema,
635
+ htmlUrl: z.string().url(),
636
+ title: z.string().nullable(),
637
+ state: githubEntityLiveStateSchema.nullable(),
638
+ number: z.number().int().nullable()
639
+ });
640
+ z.object({ items: z.array(chatGithubEntitySchema) });
601
641
  const clientStatusSchema = z.enum(["connected", "disconnected"]);
602
642
  /**
603
643
  * Auth health channel surfaced to the Web admin dashboard. Computed
@@ -771,10 +811,20 @@ const contextTreeSummarySchema = z.object({
771
811
  removedCount: z.number().int().nonnegative(),
772
812
  changedNodeCount: z.number().int().nonnegative()
773
813
  });
814
+ const contextTreeUsageEventSchema = z.object({
815
+ id: z.string(),
816
+ agentId: z.string(),
817
+ agentName: z.string(),
818
+ agentAvatarColorToken: z.string().nullable(),
819
+ chatId: z.string().nullable(),
820
+ chatTitle: z.string().nullable(),
821
+ createdAt: z.string()
822
+ });
774
823
  const contextTreeUsageSummarySchema = z.object({
775
824
  windowDays: z.number().int().positive(),
776
825
  agentCount: z.number().int().nonnegative(),
777
- usageCount: z.number().int().nonnegative()
826
+ usageCount: z.number().int().nonnegative(),
827
+ recentEvents: z.array(contextTreeUsageEventSchema)
778
828
  });
779
829
  z.object({
780
830
  repo: z.string().nullable(),
@@ -1031,23 +1081,41 @@ z.object({
1031
1081
  });
1032
1082
  z.object({ token: z.string().min(1) });
1033
1083
  z.object({}).optional();
1034
- const meChatFilterSchema = z.enum([
1035
- "all",
1036
- "unread",
1037
- "watching"
1038
- ]);
1084
+ const meChatFilterSchema = z.enum(["all", "unread"]);
1039
1085
  const meChatMembershipKindSchema = z.enum(["participant", "watching"]);
1040
1086
  const chatEngagementViewSchema = z.enum([
1041
1087
  "active",
1042
1088
  "archived",
1043
1089
  "all"
1044
1090
  ]);
1091
+ /**
1092
+ * Coerce a CSV string like `"manual,pr,issue"` to an array of trimmed
1093
+ * non-empty tokens. Lets the wire accept both repeated query params
1094
+ * (`?origin=manual&origin=pr`) and the comma-joined form the workspace
1095
+ * URL uses (`?origin=manual,pr`). Returns `undefined` when the input is
1096
+ * missing or empty so the caller can treat "no filter" and "filter to
1097
+ * empty array" identically.
1098
+ */
1099
+ function csvArrayPreprocess(input) {
1100
+ if (typeof input !== "string") return input;
1101
+ const trimmed = input.trim();
1102
+ if (trimmed === "") return void 0;
1103
+ return trimmed.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
1104
+ }
1045
1105
  z.object({
1046
1106
  cursor: z.string().optional(),
1047
1107
  limit: z.coerce.number().int().min(1).max(200).default(50),
1048
1108
  filter: meChatFilterSchema.default("all"),
1049
1109
  engagement: chatEngagementViewSchema.default("active"),
1050
- source: chatSourceSchema.optional()
1110
+ origin: z.preprocess(csvArrayPreprocess, z.array(chatSourceSchema).optional()),
1111
+ with: z.preprocess(csvArrayPreprocess, z.array(z.string().min(1)).optional()),
1112
+ watching: z.preprocess((v) => {
1113
+ if (typeof v === "string") {
1114
+ if (v === "1" || v.toLowerCase() === "true") return true;
1115
+ if (v === "0" || v.toLowerCase() === "false" || v === "") return false;
1116
+ }
1117
+ return v;
1118
+ }, z.boolean().optional())
1051
1119
  });
1052
1120
  const meChatParticipantSchema = z.object({
1053
1121
  agentId: z.string(),
@@ -1080,6 +1148,8 @@ const meChatRowSchema = z.object({
1080
1148
  chatId: z.string(),
1081
1149
  type: z.string(),
1082
1150
  membershipKind: meChatMembershipKindSchema,
1151
+ source: chatSourceSchema.default("manual"),
1152
+ entityType: githubEntityTypeSchema.nullable().default(null),
1083
1153
  title: z.string(),
1084
1154
  topic: z.string().nullable(),
1085
1155
  participants: z.array(meChatParticipantSchema),
@@ -1347,34 +1417,16 @@ z.object({
1347
1417
  }),
1348
1418
  mentionedUser: z.string().optional()
1349
1419
  });
1350
- const notificationTypeSchema = z.enum([
1420
+ z.enum([
1351
1421
  "agent_error",
1352
1422
  "agent_blocked",
1353
1423
  "agent_stale"
1354
1424
  ]);
1355
- const notificationSeveritySchema = z.enum([
1425
+ z.enum([
1356
1426
  "high",
1357
1427
  "medium",
1358
1428
  "low"
1359
1429
  ]);
1360
- z.object({
1361
- id: z.string(),
1362
- organizationId: z.string(),
1363
- type: notificationTypeSchema,
1364
- severity: notificationSeveritySchema,
1365
- agentId: z.string().nullable(),
1366
- chatId: z.string().nullable(),
1367
- message: z.string(),
1368
- read: z.boolean(),
1369
- createdAt: z.string()
1370
- });
1371
- z.object({
1372
- limit: z.coerce.number().int().min(1).max(100).default(20),
1373
- cursor: z.string().optional(),
1374
- severity: notificationSeveritySchema.optional(),
1375
- read: z.enum(["true", "false"]).transform((v) => v === "true").optional(),
1376
- agentId: z.string().optional()
1377
- });
1378
1430
  z.object({ next: z.string().max(256).optional() });
1379
1431
  z.object({
1380
1432
  code: z.string().min(1),
@@ -1403,8 +1455,8 @@ z.object({
1403
1455
  *
1404
1456
  * Each namespace has three schemas:
1405
1457
  * - `storage` — what is persisted in `organization_settings.value`. For
1406
- * namespaces with secrets, the storage schema names the *cipher* field
1407
- * (e.g. `webhookSecretCipher`); plaintext never touches the row.
1458
+ * namespaces with secrets, the storage schema names the *cipher* field;
1459
+ * plaintext never touches the row.
1408
1460
  * - `input` — what the admin API accepts in PUT bodies. For namespaces
1409
1461
  * with secrets, `webhookSecret` is plaintext; the service layer
1410
1462
  * encrypts it before merging into storage.
@@ -1,3 +1,3 @@
1
1
  import "./cli-fetch-BGVItZxo.mjs";
2
- import { t as bindFeishuBot } from "./feishu-BE7QRxnE.mjs";
2
+ import { t as bindFeishuBot } from "./feishu-BBUEVTnb.mjs";
3
3
  export { bindFeishuBot };
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { $ as SdkError, A as printResults, B as ClientRuntime, D as checkNodeVersion, E as checkClientConfig, F as resolveCliInvocation, H as rotateClientIdWithBackup, I as restartClientService, L as startClientService, M as getClientServiceStatus, N as installClientService, O as checkServerReachable, P as isServiceSupported, Q as FirstTreeHubSDK, R as stopClientService, V as handleClientOrgMismatch, g as formatCheckReport, h as promptMissingFields, k as checkWebSocket, m as promptAddAgent, n as deriveHubUrlFromToken, p as isInteractive, t as HubUrlDerivationError, v as onboardCheck, w as checkAgentConfigs, x as runHomeMigration, y as onboardCreate, z as uninstallClientService } from "./saas-connect-CNY9Ve5V.mjs";
1
+ import { $ as SdkError, A as printResults, B as ClientRuntime, D as checkNodeVersion, E as checkClientConfig, F as resolveCliInvocation, H as rotateClientIdWithBackup, I as restartClientService, L as startClientService, M as getClientServiceStatus, N as installClientService, O as checkServerReachable, P as isServiceSupported, Q as FirstTreeHubSDK, R as stopClientService, V as handleClientOrgMismatch, g as formatCheckReport, h as promptMissingFields, k as checkWebSocket, m as promptAddAgent, n as deriveHubUrlFromToken, p as isInteractive, t as HubUrlDerivationError, v as onboardCheck, w as checkAgentConfigs, x as runHomeMigration, y as onboardCreate, z as uninstallClientService } from "./saas-connect-gXv14p6J.mjs";
2
2
  import { i as ensureFreshAdminToken, n as AuthRefreshRateLimitedError, o as resolveAccessToken, r as ensureFreshAccessToken, s as resolveServerUrl, t as AuthRefreshFailedError } from "./bootstrap-CmkHQsnS.mjs";
3
3
  import { i as blank, s as status } from "./cli-fetch-BGVItZxo.mjs";
4
- import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-BE7QRxnE.mjs";
4
+ import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-BBUEVTnb.mjs";
5
5
  export { AuthRefreshFailedError, AuthRefreshRateLimitedError, ClientRuntime, FirstTreeHubSDK, HubUrlDerivationError, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkNodeVersion, checkServerReachable, checkWebSocket, deriveHubUrlFromToken, ensureFreshAccessToken, ensureFreshAdminToken, formatCheckReport, getClientServiceStatus, handleClientOrgMismatch, installClientService, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, restartClientService, rotateClientIdWithBackup, runHomeMigration, startClientService, status, stopClientService, uninstallClientService };
@@ -3677,6 +3677,152 @@ function configureClientLoggerForService(logDir) {
3677
3677
  //#endregion
3678
3678
  //#region ../client/dist/index.mjs
3679
3679
  /**
3680
+ * Shared scanner for plain markdown document-path tokens inside chat text.
3681
+ *
3682
+ * Runtime (client) calls this to know which `.md` paths to read off disk and
3683
+ * embed as snapshots in `metadata.documentContext`. Web calls the same scanner
3684
+ * to know which tokens to wrap in `[path](path)` so react-markdown renders
3685
+ * them as clickable links. Sharing one scanner is what guarantees that every
3686
+ * link the user sees in the UI has a matching snapshot in metadata — the
3687
+ * cache-key on both sides is the canonical path the scanner reports here.
3688
+ *
3689
+ * Skipped constructs (must stay aligned with markdown rules so we don't
3690
+ * rewrite something the renderer already treats specially):
3691
+ * - inline markdown links `[text](path.md)` (the runtime scans those
3692
+ * separately via `scanInlineMarkdownLinks`; this scanner intentionally
3693
+ * does not double-handle them)
3694
+ * - inline code spans `` `...` ``
3695
+ * - fenced code blocks (``` ``` ``` ``` / `~~~`)
3696
+ * - indented code blocks (4+ spaces or leading tab)
3697
+ * - HTML tag bodies `<...>` (so href / src attribute values aren't
3698
+ * re-linkified)
3699
+ * - reference-link definitions `[ref]: target`
3700
+ * - domain-shaped tokens like `example.com/readme.md` — these are URLs the
3701
+ * user almost certainly meant as external links, not workspace paths.
3702
+ *
3703
+ * The function returns matches WITH offsets so callers that need to rewrite
3704
+ * the source string (web's `linkifyMarkdownDocPaths`) can do so without
3705
+ * re-running the same regex.
3706
+ */
3707
+ /**
3708
+ * v1 limitation — the path-segment character class is intentionally ASCII
3709
+ * only. Unicode filenames and Windows backslash-separated paths are NOT
3710
+ * linkified. Workspaces in practice use POSIX-style ASCII paths, and the
3711
+ * runtime's `normalizeDocLinkPath` also rejects absolute paths, so this
3712
+ * matches the rest of the pipeline. Loosening the character class later
3713
+ * is straightforward but requires re-validating that we don't accidentally
3714
+ * absorb adjacent punctuation as part of a "filename".
3715
+ */
3716
+ const BARE_PATH_RE = /(^|[\s([{"'])(?<path>(?:\.{1,2}\/|\/)?(?:[A-Za-z0-9_.~+@%-]+\/)*[A-Za-z0-9_.~+@%-]+\.md(?::\d+(?::\d+)?)?)(?=$|[\s)\]}"',.;!?])/g;
3717
+ const INLINE_MARKDOWN_LINK_RE = /\[(?:[^\]\\\n]|\\.)*\]\([^)\n]*\)/g;
3718
+ const REFERENCE_LINK_DEFINITION_RE = /^\s*\[[^\]\n]+\]:\s*\S+/;
3719
+ const HTML_TAG_RE = /<\/?[A-Za-z][^>\n]*>/g;
3720
+ const FENCE_OPEN_RE = /^\s*(```|~~~)/;
3721
+ const INDENT_CODE_RE = /^(?: {4}|\t)/;
3722
+ const DOMAIN_LIKE_PREFIX_RE = /^[a-z][a-z0-9.-]*\.[a-z]{2,}\//i;
3723
+ /**
3724
+ * Scan `text` for plain `.md` path tokens that are NOT already wrapped in a
3725
+ * markdown link, code span, HTML tag, or fenced/indented code block.
3726
+ *
3727
+ * The returned tokens still need to be passed through
3728
+ * `normalizeDocLinkPath(stripLineSuffix(raw))` before they are treated as
3729
+ * canonical workspace paths.
3730
+ */
3731
+ function scanBareDocPathTokens(text) {
3732
+ const out = [];
3733
+ const lines = text.split(/(\r?\n)/);
3734
+ let inFence = false;
3735
+ let absoluteOffset = 0;
3736
+ for (const line of lines) {
3737
+ if (line === "\n" || line === "\r\n") {
3738
+ absoluteOffset += line.length;
3739
+ continue;
3740
+ }
3741
+ if (FENCE_OPEN_RE.test(line)) {
3742
+ inFence = !inFence;
3743
+ absoluteOffset += line.length;
3744
+ continue;
3745
+ }
3746
+ if (inFence) {
3747
+ absoluteOffset += line.length;
3748
+ continue;
3749
+ }
3750
+ if (INDENT_CODE_RE.test(line) || REFERENCE_LINK_DEFINITION_RE.test(line)) {
3751
+ absoluteOffset += line.length;
3752
+ continue;
3753
+ }
3754
+ const skipRanges = findInlineSkipRanges(line);
3755
+ BARE_PATH_RE.lastIndex = 0;
3756
+ let match = BARE_PATH_RE.exec(line);
3757
+ while (match !== null) {
3758
+ const boundary = match[1] ?? "";
3759
+ const path = match.groups?.path ?? "";
3760
+ const pathStart = match.index + boundary.length;
3761
+ const pathEnd = pathStart + path.length;
3762
+ if (path && !isInsideAnyRange(pathStart, skipRanges) && !isDomainLike(path)) out.push({
3763
+ raw: path,
3764
+ start: absoluteOffset + pathStart,
3765
+ end: absoluteOffset + pathEnd
3766
+ });
3767
+ match = BARE_PATH_RE.exec(line);
3768
+ }
3769
+ absoluteOffset += line.length;
3770
+ }
3771
+ return out;
3772
+ }
3773
+ /**
3774
+ * Strip the `:line[:col]` suffix that agents often append to file references.
3775
+ * Returns the path portion (without the trailing line/column digits) so
3776
+ * callers can hand it to `normalizeDocLinkPath`. We accept and discard the
3777
+ * line/column information — preview-time line scrolling is out of scope
3778
+ * for the snapshot link path.
3779
+ */
3780
+ function stripDocPathLineSuffix(raw) {
3781
+ return /^(?<path>.*?\.md)(?::\d+(?::\d+)?)?$/i.exec(raw)?.groups?.path ?? raw;
3782
+ }
3783
+ function findInlineSkipRanges(line) {
3784
+ const ranges = [];
3785
+ for (const match of line.matchAll(INLINE_MARKDOWN_LINK_RE)) if (match.index !== void 0) ranges.push({
3786
+ start: match.index,
3787
+ end: match.index + match[0].length
3788
+ });
3789
+ for (const match of line.matchAll(HTML_TAG_RE)) if (match.index !== void 0) ranges.push({
3790
+ start: match.index,
3791
+ end: match.index + match[0].length
3792
+ });
3793
+ for (let idx = 0; idx < line.length;) {
3794
+ if (line[idx] !== "`") {
3795
+ idx += 1;
3796
+ continue;
3797
+ }
3798
+ const start = idx;
3799
+ while (line[idx] === "`") idx += 1;
3800
+ const ticks = line.slice(start, idx);
3801
+ const end = line.indexOf(ticks, idx);
3802
+ if (end === -1) continue;
3803
+ ranges.push({
3804
+ start,
3805
+ end: end + ticks.length
3806
+ });
3807
+ idx = end + ticks.length;
3808
+ }
3809
+ return ranges;
3810
+ }
3811
+ function isInsideAnyRange(index, ranges) {
3812
+ return ranges.some((range) => index >= range.start && index < range.end);
3813
+ }
3814
+ /**
3815
+ * Decide whether the first path segment looks like a bare domain name
3816
+ * (`example.com/...`) rather than a real `.md` filename. The check is
3817
+ * suppressed when the first segment already ends in `.md`, since a directory
3818
+ * named `notes.md/` is a legitimate workspace path even though it matches
3819
+ * the domain-shape regex.
3820
+ */
3821
+ function isDomainLike(path) {
3822
+ if ((path.split("/", 1).at(0) ?? "").toLowerCase().endsWith(".md")) return false;
3823
+ return DOMAIN_LIKE_PREFIX_RE.test(path);
3824
+ }
3825
+ /**
3680
3826
  * Workspace-relative markdown doc path normalisation.
3681
3827
  *
3682
3828
  * Shared by:
@@ -4226,10 +4372,7 @@ const chatMetadataSchema = z.discriminatedUnion("source", [githubChatMetadataSch
4226
4372
  const optionalChatMetadataSchema = z.union([z.object({}).strict(), chatMetadataSchema]);
4227
4373
  const chatSourceSchema = z.enum([
4228
4374
  "manual",
4229
- "github_issue",
4230
- "github_pull_request",
4231
- "github_discussion",
4232
- "github_commit",
4375
+ "github",
4233
4376
  "feishu"
4234
4377
  ]);
4235
4378
  const chatTypeSchema = z.enum(["direct", "group"]);
@@ -4258,7 +4401,9 @@ const chatParticipantDetailSchema = z.object({
4258
4401
  }).extend({
4259
4402
  name: z.string().nullable(),
4260
4403
  displayName: z.string(),
4261
- type: z.string()
4404
+ type: z.string(),
4405
+ avatarColorToken: z.string().nullable(),
4406
+ avatarImageUrl: z.string().nullable()
4262
4407
  });
4263
4408
  z.object({
4264
4409
  id: z.string(),
@@ -4279,6 +4424,47 @@ z.object({
4279
4424
  z.object({ topic: z.string().trim().max(500).nullable() });
4280
4425
  z.object({ agentId: z.string().min(1) });
4281
4426
  z.object({ agentId: z.string().min(1) });
4427
+ /**
4428
+ * Schema for `GET /api/v1/chats/:chatId/github-entities`.
4429
+ *
4430
+ * Each item describes one entity bound to the chat via
4431
+ * `github_entity_chat_mappings`. Live state (`title`, `state`) is fetched
4432
+ * per-request from the GitHub REST API at request time and NEVER
4433
+ * persisted to the database — the user explicitly opted for the
4434
+ * fresh-on-every-open model rather than a webhook-synced column.
4435
+ *
4436
+ * Live fields may be `null` when the GitHub call failed (token unavailable,
4437
+ * rate-limited, 404 on a deleted entity, etc.). The client falls back to
4438
+ * rendering `entityKey + htmlUrl` so the row is still a working link.
4439
+ */
4440
+ const githubEntityBoundViaSchema = z.enum([
4441
+ "direct",
4442
+ "fixes_link",
4443
+ "agent_created"
4444
+ ]);
4445
+ /**
4446
+ * Coarse live state. PR-specific values (`merged`, `draft`) are folded
4447
+ * into the same enum as the issue's `open` / `closed` so the UI can
4448
+ * switch on a single field. `null` means we couldn't reach GitHub for
4449
+ * this entity in this request — the row still renders, just without
4450
+ * the state badge.
4451
+ */
4452
+ const githubEntityLiveStateSchema = z.enum([
4453
+ "open",
4454
+ "closed",
4455
+ "merged",
4456
+ "draft"
4457
+ ]);
4458
+ const chatGithubEntitySchema = z.object({
4459
+ entityType: githubEntityTypeSchema,
4460
+ entityKey: z.string().min(1),
4461
+ boundVia: githubEntityBoundViaSchema,
4462
+ htmlUrl: z.string().url(),
4463
+ title: z.string().nullable(),
4464
+ state: githubEntityLiveStateSchema.nullable(),
4465
+ number: z.number().int().nullable()
4466
+ });
4467
+ z.object({ items: z.array(chatGithubEntitySchema) });
4282
4468
  const clientStatusSchema = z.enum(["connected", "disconnected"]);
4283
4469
  /**
4284
4470
  * Auth health channel surfaced to the Web admin dashboard. Computed
@@ -4452,10 +4638,20 @@ const contextTreeSummarySchema = z.object({
4452
4638
  removedCount: z.number().int().nonnegative(),
4453
4639
  changedNodeCount: z.number().int().nonnegative()
4454
4640
  });
4641
+ const contextTreeUsageEventSchema = z.object({
4642
+ id: z.string(),
4643
+ agentId: z.string(),
4644
+ agentName: z.string(),
4645
+ agentAvatarColorToken: z.string().nullable(),
4646
+ chatId: z.string().nullable(),
4647
+ chatTitle: z.string().nullable(),
4648
+ createdAt: z.string()
4649
+ });
4455
4650
  const contextTreeUsageSummarySchema = z.object({
4456
4651
  windowDays: z.number().int().positive(),
4457
4652
  agentCount: z.number().int().nonnegative(),
4458
- usageCount: z.number().int().nonnegative()
4653
+ usageCount: z.number().int().nonnegative(),
4654
+ recentEvents: z.array(contextTreeUsageEventSchema)
4459
4655
  });
4460
4656
  z.object({
4461
4657
  repo: z.string().nullable(),
@@ -4743,23 +4939,41 @@ z.object({
4743
4939
  });
4744
4940
  z.object({ token: z.string().min(1) });
4745
4941
  z.object({}).optional();
4746
- const meChatFilterSchema = z.enum([
4747
- "all",
4748
- "unread",
4749
- "watching"
4750
- ]);
4942
+ const meChatFilterSchema = z.enum(["all", "unread"]);
4751
4943
  const meChatMembershipKindSchema = z.enum(["participant", "watching"]);
4752
4944
  const chatEngagementViewSchema = z.enum([
4753
4945
  "active",
4754
4946
  "archived",
4755
4947
  "all"
4756
4948
  ]);
4949
+ /**
4950
+ * Coerce a CSV string like `"manual,pr,issue"` to an array of trimmed
4951
+ * non-empty tokens. Lets the wire accept both repeated query params
4952
+ * (`?origin=manual&origin=pr`) and the comma-joined form the workspace
4953
+ * URL uses (`?origin=manual,pr`). Returns `undefined` when the input is
4954
+ * missing or empty so the caller can treat "no filter" and "filter to
4955
+ * empty array" identically.
4956
+ */
4957
+ function csvArrayPreprocess(input) {
4958
+ if (typeof input !== "string") return input;
4959
+ const trimmed = input.trim();
4960
+ if (trimmed === "") return void 0;
4961
+ return trimmed.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4962
+ }
4757
4963
  z.object({
4758
4964
  cursor: z.string().optional(),
4759
4965
  limit: z.coerce.number().int().min(1).max(200).default(50),
4760
4966
  filter: meChatFilterSchema.default("all"),
4761
4967
  engagement: chatEngagementViewSchema.default("active"),
4762
- source: chatSourceSchema.optional()
4968
+ origin: z.preprocess(csvArrayPreprocess, z.array(chatSourceSchema).optional()),
4969
+ with: z.preprocess(csvArrayPreprocess, z.array(z.string().min(1)).optional()),
4970
+ watching: z.preprocess((v) => {
4971
+ if (typeof v === "string") {
4972
+ if (v === "1" || v.toLowerCase() === "true") return true;
4973
+ if (v === "0" || v.toLowerCase() === "false" || v === "") return false;
4974
+ }
4975
+ return v;
4976
+ }, z.boolean().optional())
4763
4977
  });
4764
4978
  const meChatParticipantSchema = z.object({
4765
4979
  agentId: z.string(),
@@ -4792,6 +5006,8 @@ const meChatRowSchema = z.object({
4792
5006
  chatId: z.string(),
4793
5007
  type: z.string(),
4794
5008
  membershipKind: meChatMembershipKindSchema,
5009
+ source: chatSourceSchema.default("manual"),
5010
+ entityType: githubEntityTypeSchema.nullable().default(null),
4795
5011
  title: z.string(),
4796
5012
  topic: z.string().nullable(),
4797
5013
  participants: z.array(meChatParticipantSchema),
@@ -5074,34 +5290,16 @@ z.object({
5074
5290
  }),
5075
5291
  mentionedUser: z.string().optional()
5076
5292
  });
5077
- const notificationTypeSchema = z.enum([
5293
+ z.enum([
5078
5294
  "agent_error",
5079
5295
  "agent_blocked",
5080
5296
  "agent_stale"
5081
5297
  ]);
5082
- const notificationSeveritySchema = z.enum([
5298
+ z.enum([
5083
5299
  "high",
5084
5300
  "medium",
5085
5301
  "low"
5086
5302
  ]);
5087
- z.object({
5088
- id: z.string(),
5089
- organizationId: z.string(),
5090
- type: notificationTypeSchema,
5091
- severity: notificationSeveritySchema,
5092
- agentId: z.string().nullable(),
5093
- chatId: z.string().nullable(),
5094
- message: z.string(),
5095
- read: z.boolean(),
5096
- createdAt: z.string()
5097
- });
5098
- z.object({
5099
- limit: z.coerce.number().int().min(1).max(100).default(20),
5100
- cursor: z.string().optional(),
5101
- severity: notificationSeveritySchema.optional(),
5102
- read: z.enum(["true", "false"]).transform((v) => v === "true").optional(),
5103
- agentId: z.string().optional()
5104
- });
5105
5303
  z.object({ next: z.string().max(256).optional() });
5106
5304
  z.object({
5107
5305
  code: z.string().min(1),
@@ -5130,8 +5328,8 @@ z.object({
5130
5328
  *
5131
5329
  * Each namespace has three schemas:
5132
5330
  * - `storage` — what is persisted in `organization_settings.value`. For
5133
- * namespaces with secrets, the storage schema names the *cipher* field
5134
- * (e.g. `webhookSecretCipher`); plaintext never touches the row.
5331
+ * namespaces with secrets, the storage schema names the *cipher* field;
5332
+ * plaintext never touches the row.
5135
5333
  * - `input` — what the admin API accepts in PUT bodies. For namespaces
5136
5334
  * with secrets, `webhookSecret` is plaintext; the service layer
5137
5335
  * encrypts it before merging into storage.
@@ -5900,6 +6098,10 @@ var FirstTreeHubSDK = class {
5900
6098
  async listChatParticipants(chatId) {
5901
6099
  return this.requestJson(`/api/v1/agent/chats/${chatId}/participants`);
5902
6100
  }
6101
+ /** Fetch Context Tree configuration for this SDK's authenticated agent. */
6102
+ async getAgentContextTreeConfig() {
6103
+ return this.requestJson("/api/v1/agent/context-tree/info");
6104
+ }
5903
6105
  queryString(options) {
5904
6106
  const params = new URLSearchParams();
5905
6107
  if (options?.limit !== void 0) params.set("limit", String(options.limit));
@@ -6646,14 +6848,19 @@ function getHandlerFactory(type) {
6646
6848
  }
6647
6849
  return factory;
6648
6850
  }
6649
- const CONTEXT_TREE_DIR = join(DEFAULT_DATA_DIR, "context-tree");
6650
- /**
6651
- * Sync the shared Context Tree git clone.
6652
- *
6653
- * Clones on first run, pulls on subsequent runs.
6654
- * Returns the binding on success, null on failure (graceful degradation).
6655
- */
6656
- async function syncContextTree(serverUrl, getAccessToken, log, userAgent) {
6851
+ const CONTEXT_TREE_REPOS_DIR = join(DEFAULT_DATA_DIR, "context-tree-repos");
6852
+ const contextTreeSyncLocks = /* @__PURE__ */ new Map();
6853
+ function contextTreeCloneDir(repo, branch) {
6854
+ return join(CONTEXT_TREE_REPOS_DIR, createHash("sha256").update(`${repo}\0${branch}`).digest("hex"));
6855
+ }
6856
+ function withContextTreeSyncLock(key, fn) {
6857
+ const next = (contextTreeSyncLocks.get(key) ?? Promise.resolve(null)).catch(() => null).then(fn).finally(() => {
6858
+ if (contextTreeSyncLocks.get(key) === next) contextTreeSyncLocks.delete(key);
6859
+ });
6860
+ contextTreeSyncLocks.set(key, next);
6861
+ return next;
6862
+ }
6863
+ async function resolveContextTreeBinding(fetchConfig, log) {
6657
6864
  try {
6658
6865
  execFileSync("git", ["--version"], { stdio: "ignore" });
6659
6866
  } catch {
@@ -6663,11 +6870,7 @@ async function syncContextTree(serverUrl, getAccessToken, log, userAgent) {
6663
6870
  let repo;
6664
6871
  let branch;
6665
6872
  try {
6666
- const config = await new FirstTreeHubSDK({
6667
- serverUrl,
6668
- getAccessToken,
6669
- userAgent
6670
- }).getContextTreeConfig();
6873
+ const config = await fetchConfig();
6671
6874
  if (!config.repo) {
6672
6875
  log("Context Tree sync skipped: not configured on server");
6673
6876
  return null;
@@ -6678,39 +6881,43 @@ async function syncContextTree(serverUrl, getAccessToken, log, userAgent) {
6678
6881
  log(`Context Tree sync skipped: failed to fetch config from server (${err instanceof Error ? err.message : String(err)})`);
6679
6882
  return null;
6680
6883
  }
6884
+ const cloneDir = contextTreeCloneDir(repo, branch);
6885
+ return withContextTreeSyncLock(cloneDir, () => syncContextTreeRepo(repo, branch, cloneDir, log));
6886
+ }
6887
+ async function syncContextTreeRepo(repo, branch, cloneDir, log) {
6681
6888
  try {
6682
- if (existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
6889
+ if (existsSync(join(cloneDir, ".git"))) {
6683
6890
  if (execFileSync("git", [
6684
6891
  "rev-parse",
6685
6892
  "--abbrev-ref",
6686
6893
  "HEAD"
6687
6894
  ], {
6688
- cwd: CONTEXT_TREE_DIR,
6895
+ cwd: cloneDir,
6689
6896
  encoding: "utf-8",
6690
6897
  timeout: 5e3
6691
6898
  }).trim() !== branch) {
6692
6899
  execFileSync("git", ["checkout", branch], {
6693
- cwd: CONTEXT_TREE_DIR,
6900
+ cwd: cloneDir,
6694
6901
  stdio: "pipe",
6695
6902
  timeout: 1e4
6696
6903
  });
6697
6904
  log(`Context Tree switched to branch ${branch}`);
6698
6905
  }
6699
6906
  execFileSync("git", ["pull", "--ff-only"], {
6700
- cwd: CONTEXT_TREE_DIR,
6907
+ cwd: cloneDir,
6701
6908
  stdio: "pipe",
6702
6909
  timeout: 3e4
6703
6910
  });
6704
6911
  log(`Context Tree updated (pull)`);
6705
6912
  } else {
6706
- mkdirSync(CONTEXT_TREE_DIR, { recursive: true });
6913
+ mkdirSync(cloneDir, { recursive: true });
6707
6914
  execFileSync("git", [
6708
6915
  "clone",
6709
6916
  "--branch",
6710
6917
  branch,
6711
6918
  "--single-branch",
6712
6919
  repo,
6713
- CONTEXT_TREE_DIR
6920
+ cloneDir
6714
6921
  ], {
6715
6922
  stdio: "pipe",
6716
6923
  timeout: 6e4
@@ -6718,7 +6925,7 @@ async function syncContextTree(serverUrl, getAccessToken, log, userAgent) {
6718
6925
  log(`Context Tree cloned from ${repo} (branch: ${branch})`);
6719
6926
  }
6720
6927
  return {
6721
- path: CONTEXT_TREE_DIR,
6928
+ path: cloneDir,
6722
6929
  repoUrl: repo,
6723
6930
  branch
6724
6931
  };
@@ -6726,28 +6933,28 @@ async function syncContextTree(serverUrl, getAccessToken, log, userAgent) {
6726
6933
  const msg = err instanceof Error ? err.message : String(err);
6727
6934
  log(`Context Tree sync failed: ${msg}`);
6728
6935
  log("Check that git credentials (SSH key or credential helper) are configured for this repo");
6729
- if ((msg.includes("cannot fast-forward") || msg.includes("not possible to fast-forward") || msg.includes("CONFLICT")) && existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
6936
+ if ((msg.includes("cannot fast-forward") || msg.includes("not possible to fast-forward") || msg.includes("CONFLICT")) && existsSync(join(cloneDir, ".git"))) {
6730
6937
  log("Diverged history detected, attempting fresh clone...");
6731
6938
  try {
6732
- rmSync(CONTEXT_TREE_DIR, {
6939
+ rmSync(cloneDir, {
6733
6940
  recursive: true,
6734
6941
  force: true
6735
6942
  });
6736
- mkdirSync(CONTEXT_TREE_DIR, { recursive: true });
6943
+ mkdirSync(cloneDir, { recursive: true });
6737
6944
  execFileSync("git", [
6738
6945
  "clone",
6739
6946
  "--branch",
6740
6947
  branch,
6741
6948
  "--single-branch",
6742
6949
  repo,
6743
- CONTEXT_TREE_DIR
6950
+ cloneDir
6744
6951
  ], {
6745
6952
  stdio: "pipe",
6746
6953
  timeout: 6e4
6747
6954
  });
6748
6955
  log("Context Tree re-cloned successfully");
6749
6956
  return {
6750
- path: CONTEXT_TREE_DIR,
6957
+ path: cloneDir,
6751
6958
  repoUrl: repo,
6752
6959
  branch
6753
6960
  };
@@ -6755,10 +6962,10 @@ async function syncContextTree(serverUrl, getAccessToken, log, userAgent) {
6755
6962
  log("Context Tree re-clone also failed, continuing without context");
6756
6963
  }
6757
6964
  }
6758
- if (existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
6965
+ if (existsSync(join(cloneDir, ".git"))) {
6759
6966
  log("Using existing Context Tree clone despite sync failure");
6760
6967
  return {
6761
- path: CONTEXT_TREE_DIR,
6968
+ path: cloneDir,
6762
6969
  repoUrl: repo,
6763
6970
  branch
6764
6971
  };
@@ -6767,6 +6974,16 @@ async function syncContextTree(serverUrl, getAccessToken, log, userAgent) {
6767
6974
  }
6768
6975
  }
6769
6976
  /**
6977
+ * Sync the Context Tree checkout for the authenticated runtime agent.
6978
+ *
6979
+ * Uses the SDK's agent-scoped `/api/v1/agent/context-tree/info` route, so the
6980
+ * binding follows the agent's own organization rather than the caller's
6981
+ * default organization. Local checkouts are still isolated per `(repo, branch)`.
6982
+ */
6983
+ async function syncAgentContextTree(sdk, log) {
6984
+ return resolveContextTreeBinding(() => sdk.getAgentContextTreeConfig(), log);
6985
+ }
6986
+ /**
6770
6987
  * Marker file written into every workspace so the Codex CLI's project-root
6771
6988
  * detection (configured via `project_root_markers: ["first-tree-workspace"]`)
6772
6989
  * stops at the workspace boundary instead of walking up the filesystem and
@@ -9813,21 +10030,23 @@ var Deduplicator = class {
9813
10030
  * offending link simply downgrades to a plain link in the UI.
9814
10031
  */
9815
10032
  async function buildMessageDocumentSnapshots(text, root) {
9816
- const links = scanInlineMarkdownLinks(text);
9817
- if (links.length === 0) return {
10033
+ const inline = scanInlineMarkdownLinks(text);
10034
+ const bare = scanBareDocPathTokens(text).map((m) => stripDocPathLineSuffix(m.raw));
10035
+ const candidates = [...inline, ...bare];
10036
+ if (candidates.length === 0) return {
9818
10037
  docs: [],
9819
10038
  skipped: 0
9820
10039
  };
9821
10040
  const rootReal = await safeRealpath(root);
9822
10041
  if (!rootReal) return {
9823
10042
  docs: [],
9824
- skipped: links.length
10043
+ skipped: candidates.length
9825
10044
  };
9826
10045
  const docs = [];
9827
10046
  let totalBytes = 0;
9828
10047
  let skipped = 0;
9829
10048
  const seenCanonical = /* @__PURE__ */ new Set();
9830
- for (const rawPath of links) {
10049
+ for (const rawPath of candidates) {
9831
10050
  if (docs.length >= 5) {
9832
10051
  skipped += 1;
9833
10052
  continue;
@@ -10722,7 +10941,7 @@ var AgentSlot = class {
10722
10941
  lastActivityMs: 0
10723
10942
  };
10724
10943
  }
10725
- async start(contextTreeBinding) {
10944
+ async start() {
10726
10945
  const sdk = (await this.clientConnection.bindAgent(this.config.agentId, this.config.runtimeType ?? this.config.type, this.config.runtimeVersion)).sdk;
10727
10946
  const agent = await sdk.register();
10728
10947
  this.logger.info({ displayName: agent.displayName }, "agent bound");
@@ -10743,6 +10962,8 @@ var AgentSlot = class {
10743
10962
  throw new Error(`Hub unreachable while loading agent config: ${msg}`);
10744
10963
  }
10745
10964
  this.inboxId = agent.inboxId;
10965
+ const contextTreeBinding = await syncAgentContextTree(sdk, (msg) => this.logger.info(msg));
10966
+ if (!contextTreeBinding) this.logger.info("context tree not configured or sync skipped — agent will start without organizational context");
10746
10967
  const onInboxDeliver = (inboxId, frame) => {
10747
10968
  if (inboxId !== this.inboxId) return;
10748
10969
  this.dispatchPushedFrame(frame).catch((err) => {
@@ -11593,14 +11814,6 @@ var ClientRuntime = class {
11593
11814
  watcher = null;
11594
11815
  debounceTimer = null;
11595
11816
  /**
11596
- * Per-org Context Tree binding resolved at `start()`. Threaded through every
11597
- * `slot.start()` so handlers can copy `AGENT.md` / root `NODE.md` into the
11598
- * agent workspace's `.agent/context/` and install the first-tree skill.
11599
- * `null` when the user has no primary org, the org has no tree configured,
11600
- * or git sync failed — handlers degrade gracefully (empty context dir).
11601
- */
11602
- contextTreeBinding = null;
11603
- /**
11604
11817
  * Directory we write auto-registered agent configs into (same path that
11605
11818
  * `first-tree-hub agent add` uses). Set by `watchAgentsDir` so the
11606
11819
  * `agent:pinned` handler knows where to materialise new configs.
@@ -11671,7 +11884,6 @@ var ClientRuntime = class {
11671
11884
  } catch (err) {
11672
11885
  print.status("⚠️", `git-mirror orphan sweep failed: ${err instanceof Error ? err.message : String(err)}`);
11673
11886
  }
11674
- this.contextTreeBinding = await syncContextTree(this.serverUrl, (opts) => ensureFreshAccessToken(opts), (msg) => print.status("[context-tree]", msg), CLI_USER_AGENT);
11675
11887
  if (this.options.currentVersion && this.options.update) this.updateManager = UpdateManager.attach(this.connection, {
11676
11888
  currentVersion: this.options.currentVersion,
11677
11889
  ...this.options.update,
@@ -11690,7 +11902,7 @@ var ClientRuntime = class {
11690
11902
  }
11691
11903
  await Promise.allSettled(this.agents.map(async (agent) => {
11692
11904
  try {
11693
- const identity = await agent.slot.start(this.contextTreeBinding);
11905
+ const identity = await agent.slot.start();
11694
11906
  print.check(true, `${agent.name}: connected`, `agent: ${identity.displayName ?? identity.agentId}`);
11695
11907
  } catch (error) {
11696
11908
  const msg = error instanceof Error ? error.message : String(error);
@@ -11817,7 +12029,7 @@ var ClientRuntime = class {
11817
12029
  startAgent(name) {
11818
12030
  const entry = this.agents.find((a) => a.name === name);
11819
12031
  if (!entry) return;
11820
- entry.slot.start(this.contextTreeBinding).then((identity) => {
12032
+ entry.slot.start().then((identity) => {
11821
12033
  print.check(true, `${name}: connected`, `agent: ${identity.displayName ?? identity.agentId}`);
11822
12034
  }).catch((err) => {
11823
12035
  const msg = err instanceof Error ? err.message : String(err);
@@ -13112,7 +13324,7 @@ async function onboardCreate(args) {
13112
13324
  }
13113
13325
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
13114
13326
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
13115
- const { bindFeishuBot } = await import("./feishu-De9_bA91.mjs");
13327
+ const { bindFeishuBot } = await import("./feishu-DLt219hJ.mjs");
13116
13328
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
13117
13329
  if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
13118
13330
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-team-foundation/first-tree-hub",
3
- "version": "0.14.6",
3
+ "version": "0.14.7-alpha.281.1",
4
4
  "type": "module",
5
5
  "description": "First Tree Hub — unified CLI for client and agent management",
6
6
  "exports": {