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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/{bootstrap-B-FRMuvL.mjs → bootstrap-D-Yf8yOc.mjs} +2 -16
  2. package/dist/cli/index.mjs +44 -41
  3. package/dist/cli-fetch--tiwKm5S.mjs +167 -0
  4. package/dist/client-By1K4VVT-DuI6EnSh.mjs +4 -0
  5. package/dist/{client-CLdRbuml-B416INrm.mjs → client-CLdRbuml-svTO0Eat.mjs} +2 -2
  6. package/dist/{dist-BLY7Bu-l.mjs → dist-BQtAQNRD.mjs} +1 -1
  7. package/dist/{dist-FuUBFTEB.mjs → dist-ClFs4WMj.mjs} +55 -1
  8. package/dist/drizzle/0032_organization_settings.sql +36 -0
  9. package/dist/drizzle/meta/_journal.json +8 -1
  10. package/dist/{feishu-GvFABWW5.mjs → feishu-AI3pwmqN.mjs} +4 -3
  11. package/dist/{getMachineId-bsd-DjLgZlll.mjs → getMachineId-bsd-DyySs8xz.mjs} +2 -2
  12. package/dist/{getMachineId-bsd-DR4-Dysy.mjs → getMachineId-bsd-c2VImogj.mjs} +2 -2
  13. package/dist/{getMachineId-darwin-CaD2juTg.mjs → getMachineId-darwin-Cl7TSzgO.mjs} +2 -2
  14. package/dist/{getMachineId-darwin-B6WCAhc4.mjs → getMachineId-darwin-DKgI8b1d.mjs} +2 -2
  15. package/dist/{getMachineId-linux-Dk3gWdQK.mjs → getMachineId-linux-1OIMWfdh.mjs} +1 -1
  16. package/dist/{getMachineId-linux-BeWHG1gK.mjs → getMachineId-linux-cT7EbP10.mjs} +1 -1
  17. package/dist/{getMachineId-unsupported-BMJQItvF.mjs → getMachineId-unsupported-CkX-YOG1.mjs} +1 -1
  18. package/dist/{getMachineId-unsupported-Bgz_Je1J.mjs → getMachineId-unsupported-CmVlhzIo.mjs} +1 -1
  19. package/dist/{getMachineId-win-vJ6VfDRI.mjs → getMachineId-win-C2cM60YT.mjs} +2 -2
  20. package/dist/{getMachineId-win-CdgcrzCW.mjs → getMachineId-win-Chl03TYe.mjs} +2 -2
  21. package/dist/index.mjs +10 -9
  22. package/dist/invitation-DWlyNb8x-BvXubk24.mjs +4 -0
  23. package/dist/{invitation-Dnn5gGGX-Ce7zbZpn.mjs → invitation-Dnn5gGGX-DXryyvRG.mjs} +1 -1
  24. package/dist/{multipart-parser-BIksYTkk.mjs → multipart-parser-QRu3OKK4.mjs} +1 -1
  25. package/dist/{observability-C3nY6Jcz-Bk7FX689.mjs → observability-BAScT_5S-gw1ODB_o.mjs} +140 -17
  26. package/dist/observability-CYsdAcoF.mjs +5 -0
  27. package/dist/{saas-connect-Df2CVAGp.mjs → saas-connect-CVoRK0Ex.mjs} +462 -214
  28. package/dist/{src-CzQ5KF6D.mjs → src-DFlbpJfU.mjs} +2 -2
  29. package/dist/web/assets/{index-CD7rTdqm.js → index-Bm6hgcvt.js} +1 -1
  30. package/dist/web/assets/{index-43trJLR8.js → index-k2bWRKc-.js} +87 -87
  31. package/dist/web/index.html +1 -1
  32. package/package.json +1 -1
  33. package/dist/client-By1K4VVT-nVOhsXBy.mjs +0 -4
  34. package/dist/invitation-DWlyNb8x-BEgoZ9k1.mjs +0 -4
  35. package/dist/observability-DttujCqj.mjs +0 -5
  36. /package/dist/{errors-BmyRwN0Y-CIZZ_sDc.mjs → errors-BmyRwN0Y-Dad3eV8F.mjs} +0 -0
  37. /package/dist/{esm-iadMkGbV.mjs → esm-Ci8E1Gtj.mjs} +0 -0
  38. /package/dist/{execAsync-pImxPKN5.mjs → execAsync-DUfRkc4a.mjs} +0 -0
  39. /package/dist/{execAsync-CCyouKZM.mjs → execAsync-YbEZSOYd.mjs} +0 -0
  40. /package/dist/{from-CaD373S1.mjs → from-DQ7eNRwu.mjs} +0 -0
  41. /package/dist/{src-DNBS5Yjj.mjs → src-aJMV60mR.mjs} +0 -0
@@ -1,5 +1,6 @@
1
1
  import { r as __exportAll } from "./chunk-BSw8zbkd.mjs";
2
2
  import { o as logFormatSchema, s as logLevelSchema } from "./logger-core-BTmvdflj-DjW8FM4T.mjs";
3
+ import { t as cliFetch } from "./cli-fetch--tiwKm5S.mjs";
3
4
  import { z } from "zod";
4
5
  import { dirname, join } from "node:path";
5
6
  import { homedir } from "node:os";
@@ -568,21 +569,6 @@ const serverConfigSchema = defineConfig({
568
569
  refreshTokenExpiry: field(z.string().default("30d"), { env: "FIRST_TREE_HUB_AUTH_REFRESH_TOKEN_EXPIRY" }),
569
570
  connectTokenExpiry: field(z.string().default("10m"), { env: "FIRST_TREE_HUB_AUTH_CONNECT_TOKEN_EXPIRY" })
570
571
  },
571
- contextTree: optional({
572
- repo: field(z.string().optional(), {
573
- env: "FIRST_TREE_HUB_CONTEXT_TREE_REPO",
574
- prompt: { message: "Context Tree repo URL (e.g. https://github.com/org/first-tree):" }
575
- }),
576
- localPath: field(z.string().optional(), { env: "FIRST_TREE_HUB_CONTEXT_TREE_PATH" }),
577
- branch: field(z.string().default("main"))
578
- }),
579
- github: {
580
- webhookSecret: field(z.string().optional(), {
581
- env: "FIRST_TREE_HUB_GITHUB_WEBHOOK_SECRET",
582
- secret: true
583
- }),
584
- allowedOrg: field(z.string().optional(), { env: "FIRST_TREE_HUB_GITHUB_ALLOWED_ORG" })
585
- },
586
572
  oauth: optional({ github: optional({
587
573
  clientId: field(z.string(), { env: "FIRST_TREE_HUB_GITHUB_OAUTH_CLIENT_ID" }),
588
574
  clientSecret: field(z.string(), {
@@ -784,7 +770,7 @@ async function ensureFreshAccessToken(opts) {
784
770
  if (!isTokenStale(creds.accessToken, minValidityMs)) return creds.accessToken;
785
771
  if (inflightRefresh) return inflightRefresh;
786
772
  inflightRefresh = (async () => {
787
- const res = await fetch(`${creds.serverUrl}/api/v1/auth/refresh`, {
773
+ const res = await cliFetch(`${creds.serverUrl}/api/v1/auth/refresh`, {
788
774
  method: "POST",
789
775
  headers: { "Content-Type": "application/json" },
790
776
  body: JSON.stringify({ refreshToken: creds.refreshToken }),
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import "../observability-C3nY6Jcz-Bk7FX689.mjs";
3
- import { $ as findStaleAliases, A as checkDatabase, B as installClientService, C as runHomeMigration, D as checkAgentConfigs, E as runMigrations, F as checkServerReachable, G as stopClientService, I as checkWebSocket, L as printResults, M as checkNodeVersion, N as checkServerConfig, O as checkBackgroundService, P as checkServerHealth, R as reconcileAgentConfigs, S as saveOnboardState, T as migrateLocalAgentDirs, U as restartClientService, V as isServiceSupported, W as startClientService, X as ClientRuntime, Y as stopPostgres, Z as handleClientOrgMismatch, _ as promptMissingFields, _t as probeCapabilities, a as declineUpdate, at as fail, b as onboardCheck, c as detectInstallMode, ct as print, d as startServer, dt as ClientOrgMismatchError, et as formatStaleReason, f as COMMAND_VERSION, ft as ClientUserMismatchError, g as promptAddAgent, gt as cleanWorkspaces, h as isInteractive, ht as SessionRegistry, i as createExecuteUpdate, it as resolveReplyToFromEnv, j as checkDocker, k as checkClientConfig, l as fetchLatestVersion, lt as setJsonMode, m as uploadClientCapabilities, mt as SdkError, nt as createOwner, o as promptUpdate, ot as success, p as reconcileLocalRuntimeProviders, pt as FirstTreeHubSDK, r as registerSaaSConnectCommand, s as PACKAGE_NAME, tt as removeLocalAgent, u as installGlobalLatest, v as formatCheckReport, vt as applyClientLoggerConfig, w as createApiNameResolver, x as onboardCreate, y as loadOnboardState, yt as configureClientLoggerForService, z as getClientServiceStatus } from "../saas-connect-Df2CVAGp.mjs";
2
+ import "../observability-BAScT_5S-gw1ODB_o.mjs";
3
+ import { $ as formatStaleReason, A as checkDocker, B as isServiceSupported, C as createApiNameResolver, D as checkBackgroundService, E as checkAgentConfigs, F as checkWebSocket, H as restartClientService, I as printResults, J as stopPostgres, L as reconcileAgentConfigs, M as checkServerConfig, N as checkServerHealth, O as checkClientConfig, P as checkServerReachable, Q as findStaleAliases, R as getClientServiceStatus, S as runHomeMigration, T as runMigrations, U as startClientService, W as stopClientService, X as handleClientOrgMismatch, Y as ClientRuntime, _ as formatCheckReport, a as declineUpdate, at as success, b as onboardCreate, c as detectInstallMode, ct as FirstTreeHubSDK, d as startServer, dt as cleanWorkspaces, et as removeLocalAgent, f as reconcileLocalRuntimeProviders, ft as probeCapabilities, g as promptMissingFields, h as promptAddAgent, i as createExecuteUpdate, it as fail, j as checkNodeVersion, k as checkDatabase, l as fetchLatestVersion, lt as SdkError, m as isInteractive, mt as configureClientLoggerForService, o as promptUpdate, ot as ClientOrgMismatchError, p as uploadClientCapabilities, pt as applyClientLoggerConfig, r as registerSaaSConnectCommand, rt as resolveReplyToFromEnv, s as PACKAGE_NAME, st as ClientUserMismatchError, tt as createOwner, u as installGlobalLatest, ut as SessionRegistry, v as loadOnboardState, w as migrateLocalAgentDirs, x as saveOnboardState, y as onboardCheck, z as installClientService } from "../saas-connect-CVoRK0Ex.mjs";
4
4
  import "../logger-core-BTmvdflj-DjW8FM4T.mjs";
5
- import { C as resetConfigMeta, E as setConfigValue, S as resetConfig, T as serverConfigSchema, _ as getConfigValue, a as ensureFreshAdminToken, c as resolveServerUrl, d as DEFAULT_CONFIG_DIR, f as DEFAULT_DATA_DIR, h as clientConfigSchema, i as ensureFreshAccessToken, l as saveAgentConfig, m as agentConfigSchema, o as loadCredentials, p as DEFAULT_HOME_DIR, u as saveCredentials, v as initConfig, w as resolveConfigReadonly, x as readConfigFile, y as loadAgents } from "../bootstrap-B-FRMuvL.mjs";
6
- import "../dist-FuUBFTEB.mjs";
7
- import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-GvFABWW5.mjs";
8
- import "../errors-BmyRwN0Y-CIZZ_sDc.mjs";
9
- import "../client-CLdRbuml-B416INrm.mjs";
10
- import "../src-DNBS5Yjj.mjs";
11
- import "../invitation-Dnn5gGGX-Ce7zbZpn.mjs";
5
+ import { C as resetConfigMeta, E as setConfigValue, S as resetConfig, T as serverConfigSchema, _ as getConfigValue, a as ensureFreshAdminToken, c as resolveServerUrl, d as DEFAULT_CONFIG_DIR, f as DEFAULT_DATA_DIR, h as clientConfigSchema, i as ensureFreshAccessToken, l as saveAgentConfig, m as agentConfigSchema, o as loadCredentials, p as DEFAULT_HOME_DIR, u as saveCredentials, v as initConfig, w as resolveConfigReadonly, x as readConfigFile, y as loadAgents } from "../bootstrap-D-Yf8yOc.mjs";
6
+ import { a as print, n as CLI_USER_AGENT, o as setJsonMode, r as COMMAND_VERSION, t as cliFetch } from "../cli-fetch--tiwKm5S.mjs";
7
+ import "../dist-ClFs4WMj.mjs";
8
+ import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-AI3pwmqN.mjs";
9
+ import "../errors-BmyRwN0Y-Dad3eV8F.mjs";
10
+ import "../client-CLdRbuml-svTO0Eat.mjs";
11
+ import "../src-aJMV60mR.mjs";
12
+ import "../invitation-Dnn5gGGX-DXryyvRG.mjs";
12
13
  import { join } from "node:path";
13
14
  import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
14
15
  import * as semver from "semver";
@@ -16,7 +17,7 @@ import { Command } from "commander";
16
17
  import { confirm, input, password, select } from "@inquirer/prompts";
17
18
  //#region src/commands/agent-config.ts
18
19
  async function resolveAgentRecord(serverUrl, adminToken, agentName) {
19
- const res = await fetch(`${serverUrl}/api/v1/me/managed-agents`, {
20
+ const res = await cliFetch(`${serverUrl}/api/v1/me/managed-agents`, {
20
21
  headers: { Authorization: `Bearer ${adminToken}` },
21
22
  signal: AbortSignal.timeout(1e4)
22
23
  });
@@ -27,7 +28,7 @@ async function resolveAgentRecord(serverUrl, adminToken, agentName) {
27
28
  }
28
29
  async function adminFetch(url, init) {
29
30
  const { adminToken, headers, ...rest } = init;
30
- const res = await fetch(url, {
31
+ const res = await cliFetch(url, {
31
32
  ...rest,
32
33
  headers: {
33
34
  Authorization: `Bearer ${adminToken}`,
@@ -240,7 +241,8 @@ function createSdk(agentName) {
240
241
  return new FirstTreeHubSDK({
241
242
  serverUrl,
242
243
  getAccessToken: (opts) => ensureFreshAccessToken(opts),
243
- agentId
244
+ agentId,
245
+ userAgent: CLI_USER_AGENT
244
246
  });
245
247
  }
246
248
  function handleSdkError(error) {
@@ -276,7 +278,7 @@ function readStdin() {
276
278
  });
277
279
  }
278
280
  async function resolveAgent(serverUrl, adminToken, agentName) {
279
- const res = await fetch(`${serverUrl}/api/v1/me/managed-agents`, {
281
+ const res = await cliFetch(`${serverUrl}/api/v1/me/managed-agents`, {
280
282
  headers: { Authorization: `Bearer ${adminToken}` },
281
283
  signal: AbortSignal.timeout(1e4)
282
284
  });
@@ -339,7 +341,8 @@ function registerAgentCommands(program) {
339
341
  const clientId = readClientId();
340
342
  const sdk = new FirstTreeHubSDK({
341
343
  serverUrl,
342
- getAccessToken: (opts) => ensureFreshAccessToken(opts)
344
+ getAccessToken: (opts) => ensureFreshAccessToken(opts),
345
+ userAgent: CLI_USER_AGENT
343
346
  });
344
347
  const stale = await findStaleAliases({
345
348
  clientId,
@@ -406,7 +409,7 @@ function registerAgentCommands(program) {
406
409
  try {
407
410
  const serverUrl = resolveServerUrl(options.server);
408
411
  const token = await ensureFreshAccessToken();
409
- const res = await fetch(`${serverUrl}/api/v1/me/managed-agents`, {
412
+ const res = await cliFetch(`${serverUrl}/api/v1/me/managed-agents`, {
410
413
  headers: { Authorization: `Bearer ${token}` },
411
414
  signal: AbortSignal.timeout(1e4)
412
415
  });
@@ -433,7 +436,7 @@ function registerAgentCommands(program) {
433
436
  Authorization: `Bearer ${adminToken}`,
434
437
  "Content-Type": "application/json"
435
438
  };
436
- const meRes = await fetch(`${serverUrl}/api/v1/me`, {
439
+ const meRes = await cliFetch(`${serverUrl}/api/v1/me`, {
437
440
  headers: { Authorization: `Bearer ${adminToken}` },
438
441
  signal: AbortSignal.timeout(1e4)
439
442
  });
@@ -456,7 +459,7 @@ function registerAgentCommands(program) {
456
459
  runtimeProvider: options.runtime
457
460
  };
458
461
  if (options.displayName) createBody.displayName = options.displayName;
459
- const createRes = await fetch(`${serverUrl}/api/v1/orgs/${encodeURIComponent(orgId)}/agents`, {
462
+ const createRes = await cliFetch(`${serverUrl}/api/v1/orgs/${encodeURIComponent(orgId)}/agents`, {
460
463
  method: "POST",
461
464
  headers,
462
465
  body: JSON.stringify(createBody),
@@ -476,14 +479,14 @@ function registerAgentCommands(program) {
476
479
  try {
477
480
  const serverUrl = resolveServerUrl(options.server);
478
481
  const accessToken = await ensureFreshAccessToken();
479
- const meRes = await fetch(`${serverUrl}/api/v1/me`, {
482
+ const meRes = await cliFetch(`${serverUrl}/api/v1/me`, {
480
483
  headers: { Authorization: `Bearer ${accessToken}` },
481
484
  signal: AbortSignal.timeout(1e4)
482
485
  });
483
486
  if (!meRes.ok) fail("ME_ERROR", `Failed to fetch current member (HTTP ${meRes.status})`, 1);
484
487
  const me = await meRes.json();
485
488
  const target = await resolveAgent(serverUrl, accessToken, agentName);
486
- const patchRes = await fetch(`${serverUrl}/api/v1/agents/${target.uuid}`, {
489
+ const patchRes = await cliFetch(`${serverUrl}/api/v1/agents/${target.uuid}`, {
487
490
  method: "PATCH",
488
491
  headers: {
489
492
  Authorization: `Bearer ${accessToken}`,
@@ -526,7 +529,7 @@ function registerAgentCommands(program) {
526
529
  const serverUrl = resolveServerUrl(options.server);
527
530
  const accessToken = await ensureFreshAccessToken();
528
531
  const target = await resolveAgent(serverUrl, accessToken, agentName);
529
- const patchRes = await fetch(`${serverUrl}/api/v1/agents/${target.uuid}`, {
532
+ const patchRes = await cliFetch(`${serverUrl}/api/v1/agents/${target.uuid}`, {
530
533
  method: "PATCH",
531
534
  headers: {
532
535
  Authorization: `Bearer ${accessToken}`,
@@ -636,7 +639,7 @@ function registerAgentCommands(program) {
636
639
  try {
637
640
  const serverUrl = resolveServerUrl(options?.server);
638
641
  const accessToken = await ensureFreshAccessToken();
639
- const meRes = await fetch(`${serverUrl}/api/v1/me`, {
642
+ const meRes = await cliFetch(`${serverUrl}/api/v1/me`, {
640
643
  headers: { Authorization: `Bearer ${accessToken}` },
641
644
  signal: AbortSignal.timeout(1e4)
642
645
  });
@@ -655,7 +658,7 @@ function registerAgentCommands(program) {
655
658
  agents: []
656
659
  };
657
660
  for (const m of me.memberships) {
658
- const r = await fetch(`${serverUrl}/api/v1/orgs/${encodeURIComponent(m.organizationId)}/activity`, {
661
+ const r = await cliFetch(`${serverUrl}/api/v1/orgs/${encodeURIComponent(m.organizationId)}/activity`, {
659
662
  headers: { Authorization: `Bearer ${accessToken}` },
660
663
  signal: AbortSignal.timeout(1e4)
661
664
  });
@@ -704,8 +707,7 @@ function registerAgentCommands(program) {
704
707
  });
705
708
  agent.command("reset <name>").description("Reset agent error state to idle").option("--server <url>", "Hub server URL").action(async (name, options) => {
706
709
  try {
707
- const serverUrl = resolveServerUrl(options.server);
708
- const response = await fetch(`${serverUrl}/api/v1/agents/${name}/reset-activity`, {
710
+ const response = await cliFetch(`${resolveServerUrl(options.server)}/api/v1/agents/${name}/reset-activity`, {
709
711
  method: "POST",
710
712
  headers: { Authorization: `Bearer ${await ensureFreshAccessToken()}` },
711
713
  signal: AbortSignal.timeout(1e4)
@@ -721,8 +723,7 @@ function registerAgentCommands(program) {
721
723
  const serverUrl = resolveServerUrl(options.server);
722
724
  const adminToken = await ensureFreshAccessToken();
723
725
  const agentId = (await resolveAgent(serverUrl, adminToken, agentName)).uuid;
724
- const qs = options.state ? `?state=${options.state}` : "";
725
- const response = await fetch(`${serverUrl}/api/v1/agents/${agentId}/sessions${qs}`, {
726
+ const response = await cliFetch(`${serverUrl}/api/v1/agents/${agentId}/sessions${options.state ? `?state=${options.state}` : ""}`, {
726
727
  headers: { Authorization: `Bearer ${adminToken}` },
727
728
  signal: AbortSignal.timeout(1e4)
728
729
  });
@@ -751,7 +752,7 @@ function registerAgentCommands(program) {
751
752
  const serverUrl = resolveServerUrl(options.server);
752
753
  const adminToken = await ensureFreshAccessToken();
753
754
  const agentId = (await resolveAgent(serverUrl, adminToken, agentName)).uuid;
754
- const response = await fetch(`${serverUrl}/api/v1/agents/${agentId}/sessions/${chatId}/${cmd}`, {
755
+ const response = await cliFetch(`${serverUrl}/api/v1/agents/${agentId}/sessions/${chatId}/${cmd}`, {
755
756
  method: "POST",
756
757
  headers: { Authorization: `Bearer ${adminToken}` },
757
758
  signal: AbortSignal.timeout(1e4)
@@ -774,7 +775,7 @@ function registerAgentCommands(program) {
774
775
  "Content-Type": "application/json"
775
776
  };
776
777
  const targetAgent = await resolveAgent(serverUrl, adminToken, agentName);
777
- const dmRes = await fetch(`${serverUrl}/api/v1/agents/${targetAgent.uuid}/chats`, {
778
+ const dmRes = await cliFetch(`${serverUrl}/api/v1/agents/${targetAgent.uuid}/chats`, {
778
779
  method: "POST",
779
780
  headers,
780
781
  signal: AbortSignal.timeout(1e4)
@@ -796,7 +797,7 @@ function registerAgentCommands(program) {
796
797
  const pollMessages = async () => {
797
798
  try {
798
799
  const qs = lastSeenAt ? `?limit=50` : `?limit=10`;
799
- const msgRes = await fetch(`${serverUrl}/api/v1/chats/${dm.id}/messages${qs}`, {
800
+ const msgRes = await cliFetch(`${serverUrl}/api/v1/chats/${dm.id}/messages${qs}`, {
800
801
  headers,
801
802
  signal: AbortSignal.timeout(1e4)
802
803
  });
@@ -827,7 +828,7 @@ function registerAgentCommands(program) {
827
828
  return;
828
829
  }
829
830
  try {
830
- const sendRes = await fetch(`${serverUrl}/api/v1/chats/${dm.id}/messages`, {
831
+ const sendRes = await cliFetch(`${serverUrl}/api/v1/chats/${dm.id}/messages`, {
831
832
  method: "POST",
832
833
  headers,
833
834
  body: JSON.stringify({
@@ -963,7 +964,7 @@ function printIsolationGuide(newServerUrl) {
963
964
  * Authenticate via connect token — exchange for full JWT credentials.
964
965
  */
965
966
  async function authenticateWithToken(url, token) {
966
- const res = await fetch(`${url}/api/v1/auth/connect-token`, {
967
+ const res = await cliFetch(`${url}/api/v1/auth/connect-token`, {
967
968
  method: "POST",
968
969
  headers: { "Content-Type": "application/json" },
969
970
  body: JSON.stringify({ token }),
@@ -979,7 +980,7 @@ async function authenticateInteractive(url) {
979
980
  print.line("\n Log in to Hub:\n");
980
981
  const username = await input({ message: " Username:" });
981
982
  const pw = await password({ message: " Password:" });
982
- const loginRes = await fetch(`${url}/api/v1/auth/login`, {
983
+ const loginRes = await cliFetch(`${url}/api/v1/auth/login`, {
983
984
  method: "POST",
984
985
  headers: { "Content-Type": "application/json" },
985
986
  body: JSON.stringify({
@@ -1270,7 +1271,8 @@ function registerClientCommands(program) {
1270
1271
  });
1271
1272
  const sdk = new FirstTreeHubSDK({
1272
1273
  serverUrl,
1273
- getAccessToken: (opts) => ensureFreshAccessToken(opts)
1274
+ getAccessToken: (opts) => ensureFreshAccessToken(opts),
1275
+ userAgent: CLI_USER_AGENT
1274
1276
  });
1275
1277
  agentCheck = await reconcileAgentConfigs({
1276
1278
  clientId: cfg.client.id,
@@ -1396,7 +1398,7 @@ function registerClientCommands(program) {
1396
1398
  try {
1397
1399
  const serverUrl = resolveServerUrl(options.server);
1398
1400
  const token = await ensureFreshAccessToken();
1399
- const meRes = await fetch(`${serverUrl}/api/v1/me`, {
1401
+ const meRes = await cliFetch(`${serverUrl}/api/v1/me`, {
1400
1402
  headers: { Authorization: `Bearer ${token}` },
1401
1403
  signal: AbortSignal.timeout(1e4)
1402
1404
  });
@@ -1404,7 +1406,7 @@ function registerClientCommands(program) {
1404
1406
  const adminOrgs = (await meRes.json()).memberships.filter((m) => m.role === "admin");
1405
1407
  const clients = [];
1406
1408
  for (const m of adminOrgs) {
1407
- const r = await fetch(`${serverUrl}/api/v1/orgs/${encodeURIComponent(m.organizationId)}/clients`, {
1409
+ const r = await cliFetch(`${serverUrl}/api/v1/orgs/${encodeURIComponent(m.organizationId)}/clients`, {
1408
1410
  headers: { Authorization: `Bearer ${token}` },
1409
1411
  signal: AbortSignal.timeout(1e4)
1410
1412
  });
@@ -1453,7 +1455,7 @@ function registerClientCommands(program) {
1453
1455
  }
1454
1456
  }
1455
1457
  const token = await ensureFreshAccessToken();
1456
- const response = await fetch(`${serverUrl}/api/v1/clients/${encodeURIComponent(clientId)}/claim`, {
1458
+ const response = await cliFetch(`${serverUrl}/api/v1/clients/${encodeURIComponent(clientId)}/claim`, {
1457
1459
  method: "POST",
1458
1460
  headers: {
1459
1461
  Authorization: `Bearer ${token}`,
@@ -1471,7 +1473,8 @@ function registerClientCommands(program) {
1471
1473
  try {
1472
1474
  const sdk = new FirstTreeHubSDK({
1473
1475
  serverUrl,
1474
- getAccessToken: (opts) => ensureFreshAccessToken(opts)
1476
+ getAccessToken: (opts) => ensureFreshAccessToken(opts),
1477
+ userAgent: CLI_USER_AGENT
1475
1478
  });
1476
1479
  const stale = await findStaleAliases({
1477
1480
  clientId,
@@ -1520,7 +1523,7 @@ function registerClientCommands(program) {
1520
1523
  try {
1521
1524
  const serverUrl = resolveServerUrl(options.server);
1522
1525
  const token = await ensureFreshAccessToken();
1523
- const response = await fetch(`${serverUrl}/api/v1/clients/${encodeURIComponent(clientId)}/disconnect`, {
1526
+ const response = await cliFetch(`${serverUrl}/api/v1/clients/${encodeURIComponent(clientId)}/disconnect`, {
1524
1527
  method: "POST",
1525
1528
  headers: { Authorization: `Bearer ${token}` },
1526
1529
  signal: AbortSignal.timeout(1e4)
@@ -1667,13 +1670,13 @@ function isSecretField(schema, dotPath) {
1667
1670
  //#region src/commands/onboard.ts
1668
1671
  async function promptMissing(args) {
1669
1672
  if (!args.server) try {
1670
- const { resolveServerUrl } = await import("../bootstrap-B-FRMuvL.mjs").then((n) => n.r);
1673
+ const { resolveServerUrl } = await import("../bootstrap-D-Yf8yOc.mjs").then((n) => n.r);
1671
1674
  resolveServerUrl();
1672
1675
  } catch {
1673
1676
  args.server = await input({ message: "Hub server URL:" });
1674
1677
  saveOnboardState(args);
1675
1678
  }
1676
- const { loadCredentials } = await import("../bootstrap-B-FRMuvL.mjs").then((n) => n.r);
1679
+ const { loadCredentials } = await import("../bootstrap-D-Yf8yOc.mjs").then((n) => n.r);
1677
1680
  if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub client connect <server-url>` before onboarding.");
1678
1681
  if (!args.id) {
1679
1682
  args.id = await input({ message: "Agent ID:" });
@@ -1833,7 +1836,7 @@ function registerServerCommands(program) {
1833
1836
  server.command("status").description("Show server health and status").action(async () => {
1834
1837
  const url = process.env.FIRST_TREE_HUB_SERVER_URL ?? "http://localhost:8000";
1835
1838
  try {
1836
- const res = await fetch(`${url}/api/v1/health`);
1839
+ const res = await cliFetch(`${url}/api/v1/health`);
1837
1840
  if (res.ok) {
1838
1841
  const data = await res.json();
1839
1842
  process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
@@ -0,0 +1,167 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ //#region src/core/output.ts
5
+ /**
6
+ * Print layer — the only place CLI code should write to stdout/stderr.
7
+ *
8
+ * Contract:
9
+ * - `print.result(data)` / `print.fail(...)` emit machine-readable JSON on
10
+ * stdout / stderr respectively. Scripts pipe into `jq` and expect a clean
11
+ * envelope, so nothing else may touch stdout.
12
+ * - `print.status` / `print.check` / `print.blank` / `print.line` are
13
+ * human-friendly and go to stderr so they never pollute a redirected stdout.
14
+ * In `--json` mode they are silenced — scripted consumers only care about
15
+ * the envelope.
16
+ */
17
+ let jsonMode = false;
18
+ function setJsonMode(enabled) {
19
+ jsonMode = enabled;
20
+ }
21
+ function result(data) {
22
+ process.stdout.write(`${JSON.stringify({
23
+ ok: true,
24
+ data
25
+ })}\n`);
26
+ }
27
+ function fail(code, message, exitCode = 1) {
28
+ process.stderr.write(`${JSON.stringify({
29
+ ok: false,
30
+ error: {
31
+ code,
32
+ message
33
+ }
34
+ })}\n`);
35
+ process.exit(exitCode);
36
+ }
37
+ function status(label, message) {
38
+ if (jsonMode) return;
39
+ process.stderr.write(` ${label.padEnd(20)} ${message}\n`);
40
+ }
41
+ function check(pass, label, detail = "") {
42
+ if (jsonMode) return;
43
+ const icon = pass ? "✓" : "✗";
44
+ const tail = detail ? ` ${detail}` : "";
45
+ process.stderr.write(` ${icon} ${label.padEnd(22)}${tail}\n`);
46
+ }
47
+ function blank() {
48
+ if (jsonMode) return;
49
+ process.stderr.write("\n");
50
+ }
51
+ /**
52
+ * Generic stderr writer for pre-formatted human text (multi-line tables,
53
+ * interactive prompts). Prefer `status` / `check` when the text fits; this
54
+ * exists so the `--json` mode gate can silence arbitrary human chatter.
55
+ */
56
+ function line(text) {
57
+ if (jsonMode) return;
58
+ process.stderr.write(text);
59
+ }
60
+ const print = {
61
+ result,
62
+ fail,
63
+ status,
64
+ check,
65
+ blank,
66
+ line
67
+ };
68
+ //#endregion
69
+ //#region src/core/version.ts
70
+ /**
71
+ * Version of the consumer-facing `@agent-team-foundation/first-tree-hub`
72
+ * package. Read once at module load so the CLI, client runtime, and server
73
+ * bootstrap all quote the same string.
74
+ *
75
+ * Path-based lookups (`require("../../package.json")`) do not survive the
76
+ * tsdown bundle: the source lives at `src/core/version.ts` but every
77
+ * emitted chunk lands in `dist/` — shifting the relative depth by one and
78
+ * pointing at `packages/package.json` instead of our own manifest (the
79
+ * v0.9.1 "Cannot find module ../../package.json" crash). Walk up from this
80
+ * module's URL and accept the first `package.json` whose `name` matches, so
81
+ * dev runs (`tsx src/cli/index.ts`) and the published bundle
82
+ * (`dist/cli/index.mjs`) both resolve the same file.
83
+ */
84
+ const PACKAGE_NAME = "@agent-team-foundation/first-tree-hub";
85
+ /**
86
+ * Sentinel returned when the walker exhausts every parent directory without
87
+ * finding our manifest. Deliberately NOT valid SemVer so the client-side
88
+ * `UpdateManager` drops into its `semver.valid(current) === false` warn-and-
89
+ * skip branch instead of treating it as `< target` and triggering a spurious
90
+ * self-update loop (the scenario where the startup crash this module fixes
91
+ * would otherwise quietly reincarnate as repeated `npm install -g @latest`).
92
+ */
93
+ const UNRESOLVED_VERSION = "unknown";
94
+ /**
95
+ * Exported for tests. Walks up from `moduleUrl`'s directory looking for a
96
+ * `package.json` whose `name` field equals {@link PACKAGE_NAME}. Returns
97
+ * {@link UNRESOLVED_VERSION} as a last-resort fallback so the CLI never
98
+ * crashes on a missing manifest.
99
+ */
100
+ function resolveCommandVersion(moduleUrl = import.meta.url) {
101
+ let dir = dirname(fileURLToPath(moduleUrl));
102
+ for (let i = 0; i < 10; i++) {
103
+ try {
104
+ const pkg = JSON.parse(readFileSync(resolve(dir, "package.json"), "utf8"));
105
+ if (pkg.name === PACKAGE_NAME && typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
106
+ } catch (err) {
107
+ const code = err.code;
108
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
109
+ const message = err instanceof Error ? err.message : String(err);
110
+ print.line(`[first-tree-hub] warning: could not read ${dir}/package.json: ${message}\n`);
111
+ }
112
+ }
113
+ const parent = dirname(dir);
114
+ if (parent === dir) break;
115
+ dir = parent;
116
+ }
117
+ return UNRESOLVED_VERSION;
118
+ }
119
+ const COMMAND_VERSION = resolveCommandVersion();
120
+ /**
121
+ * `User-Agent` string sent on every CLI-originated HTTP request (SDK fetches,
122
+ * `/auth/refresh`, etc.). Without this Node defaults to `User-Agent: node`,
123
+ * which hides install / version / platform context from server-side trace
124
+ * backends — see issue #246. The format follows RFC 7231 §5.5.3 conventions
125
+ * (`product/version (comment)`).
126
+ */
127
+ const CLI_USER_AGENT = `first-tree-hub-cli/${COMMAND_VERSION} (${process.platform} ${process.arch})`;
128
+ //#endregion
129
+ //#region src/core/cli-fetch.ts
130
+ /**
131
+ * Drop-in `fetch` wrapper that stamps `User-Agent: first-tree-hub-cli/<version> (<platform> <arch>)`
132
+ * on every request.
133
+ *
134
+ * Issue #246: every CLI-originated HTTP request must carry a stable
135
+ * `User-Agent` so trace backends can group failures (401 / 429 / 5xx) by
136
+ * install. Plain `globalThis.fetch` defaults to `User-Agent: node`, which
137
+ * collapses every install into a single bucket. Centralising the header
138
+ * here means new direct-fetch sites pick the right UA up automatically —
139
+ * no need to remember at each call site.
140
+ *
141
+ * SDK-routed requests stamp UA via {@link FirstTreeHubSDK} (see
142
+ * `packages/client/src/sdk.ts`). This helper is for the
143
+ * direct-`fetch` paths the SDK doesn't cover (auth bootstrap,
144
+ * doctor probes, raw admin calls).
145
+ *
146
+ * Header precedence: caller-provided `User-Agent` in `init.headers` wins,
147
+ * so callers can override (e.g. tests injecting a deterministic UA).
148
+ */
149
+ function cliFetch(input, init) {
150
+ const merged = mergeHeaders(init?.headers);
151
+ return fetch(input, {
152
+ ...init,
153
+ headers: merged
154
+ });
155
+ }
156
+ function mergeHeaders(provided) {
157
+ const out = {};
158
+ if (provided) if (provided instanceof Headers) provided.forEach((v, k) => {
159
+ out[k] = v;
160
+ });
161
+ else if (Array.isArray(provided)) for (const [k, v] of provided) out[k] = v;
162
+ else Object.assign(out, provided);
163
+ if (!Object.keys(out).some((k) => k.toLowerCase() === "user-agent")) out["User-Agent"] = CLI_USER_AGENT;
164
+ return out;
165
+ }
166
+ //#endregion
167
+ export { print as a, blank as i, CLI_USER_AGENT as n, setJsonMode as o, COMMAND_VERSION as r, status as s, cliFetch as t };
@@ -0,0 +1,4 @@
1
+ import "./dist-ClFs4WMj.mjs";
2
+ import "./errors-BmyRwN0Y-Dad3eV8F.mjs";
3
+ import { y as listMyPinnedAgents } from "./client-CLdRbuml-svTO0Eat.mjs";
4
+ export { listMyPinnedAgents };
@@ -1,5 +1,5 @@
1
- import { C as clientCapabilitiesSchema } from "./dist-FuUBFTEB.mjs";
2
- import { a as ConflictError, i as ClientUserMismatchError, l as organizations, n as BadRequestError, s as NotFoundError, u as users } from "./errors-BmyRwN0Y-CIZZ_sDc.mjs";
1
+ import { w as clientCapabilitiesSchema } from "./dist-ClFs4WMj.mjs";
2
+ import { a as ConflictError, i as ClientUserMismatchError, l as organizations, n as BadRequestError, s as NotFoundError, u as users } from "./errors-BmyRwN0Y-Dad3eV8F.mjs";
3
3
  import { and, eq, inArray, ne, sql } from "drizzle-orm";
4
4
  import { index, integer, jsonb, pgTable, text, timestamp, unique } from "drizzle-orm/pg-core";
5
5
  //#region ../server/dist/client-CLdRbuml.mjs
@@ -1,5 +1,5 @@
1
1
  import { i as __require, t as __commonJSMin } from "./chunk-BSw8zbkd.mjs";
2
- import { t as require_src } from "./src-DNBS5Yjj.mjs";
2
+ import { t as require_src } from "./src-aJMV60mR.mjs";
3
3
  //#region ../../node_modules/.pnpm/agent-base@7.1.4/node_modules/agent-base/dist/helpers.js
4
4
  var require_helpers = /* @__PURE__ */ __commonJSMin(((exports) => {
5
5
  var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
@@ -1140,6 +1140,60 @@ const githubDevCallbackQuerySchema = z.object({
1140
1140
  displayName: z.string().optional(),
1141
1141
  next: z.string().max(256).optional()
1142
1142
  });
1143
+ /**
1144
+ * Per-organization settings — schemas, namespaces, and the registry that
1145
+ * dispatches `(orgId, namespace)` lookups to the right validator.
1146
+ *
1147
+ * Each namespace has three schemas:
1148
+ * - `storage` — what is persisted in `organization_settings.value`. For
1149
+ * namespaces with secrets, the storage schema names the *cipher* field
1150
+ * (e.g. `webhookSecretCipher`); plaintext never touches the row.
1151
+ * - `input` — what the admin API accepts in PUT bodies. For namespaces
1152
+ * with secrets, `webhookSecret` is plaintext; the service layer
1153
+ * encrypts it before merging into storage.
1154
+ * - `output` — what GET returns. Secrets are replaced by a boolean
1155
+ * `…Configured` flag — plaintext is never echoed.
1156
+ *
1157
+ * Adding a new per-org config group:
1158
+ * 1. Define three schemas (storage / input / output).
1159
+ * 2. Add a key to `ORG_SETTINGS_NAMESPACES`.
1160
+ * 3. Done. No DB migration, no new API route.
1161
+ */
1162
+ const orgContextTreeStorageSchema = z.object({
1163
+ repo: z.string().url().optional(),
1164
+ branch: z.string().default("main")
1165
+ });
1166
+ const orgContextTreeInputSchema = z.object({
1167
+ repo: z.string().url().min(1).nullish(),
1168
+ branch: z.string().min(1).nullish()
1169
+ });
1170
+ const orgContextTreeOutputSchema = z.object({
1171
+ repo: z.string().optional(),
1172
+ branch: z.string().optional()
1173
+ });
1174
+ const orgGithubIntegrationStorageSchema = z.object({ webhookSecretCipher: z.string().optional() });
1175
+ const orgGithubIntegrationInputSchema = z.object({ webhookSecret: z.string().min(1).nullish() });
1176
+ const orgGithubIntegrationOutputSchema = z.object({
1177
+ webhookSecretConfigured: z.boolean(),
1178
+ webhookUrl: z.string()
1179
+ });
1180
+ const ORG_SETTINGS_NAMESPACES = {
1181
+ context_tree: {
1182
+ storage: orgContextTreeStorageSchema,
1183
+ input: orgContextTreeInputSchema,
1184
+ output: orgContextTreeOutputSchema
1185
+ },
1186
+ github_integration: {
1187
+ storage: orgGithubIntegrationStorageSchema,
1188
+ input: orgGithubIntegrationInputSchema,
1189
+ output: orgGithubIntegrationOutputSchema
1190
+ }
1191
+ };
1192
+ const ORG_SETTINGS_NAMESPACE_KEYS = Object.keys(ORG_SETTINGS_NAMESPACES);
1193
+ z.enum(ORG_SETTINGS_NAMESPACE_KEYS);
1194
+ function isOrgSettingNamespace(value) {
1195
+ return typeof value === "string" && ORG_SETTINGS_NAMESPACE_KEYS.includes(value);
1196
+ }
1143
1197
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1144
1198
  z.object({
1145
1199
  name: z.string().min(2).max(50).regex(/^[a-z0-9][a-z0-9-]*$/, "Must start with a letter or digit and contain only lowercase alphanumeric and hyphens").refine((v) => !UUID_PATTERN.test(v), "Name must not be a UUID format"),
@@ -1467,4 +1521,4 @@ z.object({
1467
1521
  capabilities: serverCapabilitiesSchema.optional()
1468
1522
  }).passthrough();
1469
1523
  //#endregion
1470
- export { notificationQuerySchema as $, createChatSchema as A, githubDevCallbackQuerySchema as B, clientCapabilitiesSchema as C, updateTaskStatusSchema as Ct, createAdapterConfigSchema as D, contextTreeSnapshotSchema as E, defaultRuntimeConfigPayload as F, inboxPollQuerySchema as G, imageInlineContentSchema as H, delegateFeishuUserSchema as I, joinByInvitationSchema as J, isRedactedEnvValue as K, dryRunAgentRuntimeConfigSchema as L, createMemberSchema as M, createOrgFromMeSchema as N, createAdapterMappingSchema as O, createTaskSchema as P, messageSourceSchema as Q, extractMentions as R, agentTypeSchema as S, updateOrganizationSchema as St, connectTokenExchangeSchema as T, inboxAckFrameSchema as U, githubStartQuerySchema as V, inboxDeliverFrameSchema as W, listMeChatsQuerySchema as X, linkTaskChatSchema as Y, loginSchema as Z, adminCreateTaskSchema as _, updateAgentRuntimeConfigSchema as _t, AGENT_STATUSES as a, scanMentionTokens as at, agentPinnedMessageSchema as b, updateClientCapabilitiesSchema as bt, DEFAULT_RUNTIME_PROVIDER as c, sendToAgentSchema as ct, TASK_HEALTH_SIGNALS as d, sessionEventSchema as dt, paginationQuerySchema as et, TASK_STATUSES as f, sessionReconcileRequestSchema as ft, addParticipantSchema as g, updateAdapterConfigSchema as gt, addMeChatParticipantsSchema as h, taskListQuerySchema as ht, AGENT_SOURCES as i, safeRedirectPath as it, createMeChatSchema as j, createAgentSchema as k, MENTION_REGEX as l, sessionCompletionMessageSchema as lt, WS_AUTH_FRAME_TIMEOUT_MS as m, stripCode as mt, AGENT_NAME_REGEX as n, refreshTokenSchema as nt, AGENT_TYPES as o, selfServiceFeishuBotSchema as ot, TASK_TERMINAL_STATUSES as p, sessionStateMessageSchema as pt, isReservedAgentName as q, AGENT_SELECTOR_HEADER as r, runtimeStateMessageSchema as rt, AGENT_VISIBILITY as s, sendMessageSchema as st, AGENT_BIND_REJECT_REASONS as t, rebindAgentSchema as tt, TASK_CREATOR_TYPES as u, sessionEventMessageSchema as ut, adminUpdateTaskSchema as v, updateAgentSchema as vt, clientRegisterSchema as w, wsAuthFrameSchema as wt, agentRuntimeConfigPayloadSchema as x, updateMemberSchema as xt, agentBindRequestSchema as y, updateChatSchema as yt, githubCallbackQuerySchema as z };
1524
+ export { loginSchema as $, createAgentSchema as A, githubCallbackQuerySchema as B, agentTypeSchema as C, updateMemberSchema as Ct, contextTreeSnapshotSchema as D, connectTokenExchangeSchema as E, wsAuthFrameSchema as Et, createTaskSchema as F, inboxDeliverFrameSchema as G, githubStartQuerySchema as H, defaultRuntimeConfigPayload as I, isRedactedEnvValue as J, inboxPollQuerySchema as K, delegateFeishuUserSchema as L, createMeChatSchema as M, createMemberSchema as N, createAdapterConfigSchema as O, createOrgFromMeSchema as P, listMeChatsQuerySchema as Q, dryRunAgentRuntimeConfigSchema as R, agentRuntimeConfigPayloadSchema as S, updateClientCapabilitiesSchema as St, clientRegisterSchema as T, updateTaskStatusSchema as Tt, imageInlineContentSchema as U, githubDevCallbackQuerySchema as V, inboxAckFrameSchema as W, joinByInvitationSchema as X, isReservedAgentName as Y, linkTaskChatSchema as Z, addParticipantSchema as _, taskListQuerySchema as _t, AGENT_STATUSES as a, runtimeStateMessageSchema as at, agentBindRequestSchema as b, updateAgentSchema as bt, DEFAULT_RUNTIME_PROVIDER as c, selfServiceFeishuBotSchema as ct, TASK_CREATOR_TYPES as d, sessionCompletionMessageSchema as dt, messageSourceSchema as et, TASK_HEALTH_SIGNALS as f, sessionEventMessageSchema as ft, addMeChatParticipantsSchema as g, stripCode as gt, WS_AUTH_FRAME_TIMEOUT_MS as h, sessionStateMessageSchema as ht, AGENT_SOURCES as i, refreshTokenSchema as it, createChatSchema as j, createAdapterMappingSchema as k, MENTION_REGEX as l, sendMessageSchema as lt, TASK_TERMINAL_STATUSES as m, sessionReconcileRequestSchema as mt, AGENT_NAME_REGEX as n, paginationQuerySchema as nt, AGENT_TYPES as o, safeRedirectPath as ot, TASK_STATUSES as p, sessionEventSchema as pt, isOrgSettingNamespace as q, AGENT_SELECTOR_HEADER as r, rebindAgentSchema as rt, AGENT_VISIBILITY as s, scanMentionTokens as st, AGENT_BIND_REJECT_REASONS as t, notificationQuerySchema as tt, ORG_SETTINGS_NAMESPACES as u, sendToAgentSchema as ut, adminCreateTaskSchema as v, updateAdapterConfigSchema as vt, clientCapabilitiesSchema as w, updateOrganizationSchema as wt, agentPinnedMessageSchema as x, updateChatSchema as xt, adminUpdateTaskSchema as y, updateAgentRuntimeConfigSchema as yt, extractMentions as z };
@@ -0,0 +1,36 @@
1
+ -- Per-organization settings, keyed by (organization_id, namespace).
2
+ --
3
+ -- Each row holds an entire group of related config as a JSONB blob; the
4
+ -- schema for each namespace lives in @agent-team-foundation/first-tree-hub-shared
5
+ -- (ORG_SETTINGS_NAMESPACES) and is enforced by the service layer on every
6
+ -- read/write. Adding a new config group means registering a new namespace +
7
+ -- Zod schema in shared — the DB does not change.
8
+ --
9
+ -- `version` is reserved for future optimistic locking (PUT with If-Match).
10
+ -- We keep the column from day one so tightening to compare-and-swap later
11
+ -- is a code-only change with no migration.
12
+ --
13
+ -- Sensitive fields inside `value` (e.g. github_integration.webhookSecret)
14
+ -- are AES-256-GCM-encrypted at the service layer using crypto.ts's
15
+ -- encryptValue / decryptValue — same pattern as adapter_configs.
16
+ --
17
+ -- ON DELETE CASCADE on organization_id: settings have no independent
18
+ -- lifecycle, deleting an org must drop them. updated_by is SET NULL so a
19
+ -- user deletion does not cascade-clobber unrelated config rows.
20
+
21
+ CREATE TABLE IF NOT EXISTS "organization_settings" (
22
+ "organization_id" text NOT NULL,
23
+ "namespace" text NOT NULL,
24
+ "value" jsonb NOT NULL DEFAULT '{}'::jsonb,
25
+ "version" integer NOT NULL DEFAULT 0,
26
+ "updated_by" text,
27
+ "updated_at" timestamp with time zone NOT NULL DEFAULT now(),
28
+ CONSTRAINT "organization_settings_pkey" PRIMARY KEY ("organization_id", "namespace"),
29
+ CONSTRAINT "organization_settings_organization_id_organizations_id_fk"
30
+ FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE CASCADE,
31
+ CONSTRAINT "organization_settings_updated_by_users_id_fk"
32
+ FOREIGN KEY ("updated_by") REFERENCES "users"("id") ON DELETE SET NULL
33
+ );
34
+
35
+ CREATE INDEX IF NOT EXISTS "idx_org_settings_namespace"
36
+ ON "organization_settings" ("namespace");
@@ -225,6 +225,13 @@
225
225
  "when": 1777939200000,
226
226
  "tag": "0031_drop_system_configs",
227
227
  "breakpoints": true
228
+ },
229
+ {
230
+ "idx": 32,
231
+ "version": "7",
232
+ "when": 1778198400000,
233
+ "tag": "0032_organization_settings",
234
+ "breakpoints": true
228
235
  }
229
236
  ]
230
- }
237
+ }