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

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.
@@ -569,6 +569,13 @@ const serverConfigSchema = defineConfig({
569
569
  refreshTokenExpiry: field(z.string().default("30d"), { env: "FIRST_TREE_HUB_AUTH_REFRESH_TOKEN_EXPIRY" }),
570
570
  connectTokenExpiry: field(z.string().default("10m"), { env: "FIRST_TREE_HUB_AUTH_CONNECT_TOKEN_EXPIRY" })
571
571
  },
572
+ contextTreeSync: optional({
573
+ githubToken: field(z.string(), {
574
+ env: "FIRST_TREE_HUB_CONTEXT_TREE_GITHUB_TOKEN",
575
+ secret: true
576
+ }),
577
+ githubTokenRepos: field(z.string().optional(), { env: "FIRST_TREE_HUB_CONTEXT_TREE_GITHUB_TOKEN_REPOS" })
578
+ }),
572
579
  oauth: optional({ github: optional({
573
580
  clientId: field(z.string(), { env: "FIRST_TREE_HUB_GITHUB_OAUTH_CLIENT_ID" }),
574
581
  clientSecret: field(z.string(), {
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
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";
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-CO554S-V.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-D-Yf8yOc.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-C_K2CKXC.mjs";
6
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";
7
+ import "../dist-CfvCT4E0.mjs";
8
+ import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-DbSvp9UH.mjs";
9
+ import "../errors-CF5evtJt-B0NTIVPt.mjs";
10
+ import "../client-DqdGiggm-NQoGZ2vM.mjs";
11
11
  import "../src-aJMV60mR.mjs";
12
- import "../invitation-Dnn5gGGX-DXryyvRG.mjs";
12
+ import "../invitation-Bg0TRiyx-BsZH4GCS.mjs";
13
13
  import { join } from "node:path";
14
14
  import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
15
15
  import * as semver from "semver";
@@ -1670,13 +1670,13 @@ function isSecretField(schema, dotPath) {
1670
1670
  //#region src/commands/onboard.ts
1671
1671
  async function promptMissing(args) {
1672
1672
  if (!args.server) try {
1673
- const { resolveServerUrl } = await import("../bootstrap-D-Yf8yOc.mjs").then((n) => n.r);
1673
+ const { resolveServerUrl } = await import("../bootstrap-C_K2CKXC.mjs").then((n) => n.r);
1674
1674
  resolveServerUrl();
1675
1675
  } catch {
1676
1676
  args.server = await input({ message: "Hub server URL:" });
1677
1677
  saveOnboardState(args);
1678
1678
  }
1679
- const { loadCredentials } = await import("../bootstrap-D-Yf8yOc.mjs").then((n) => n.r);
1679
+ const { loadCredentials } = await import("../bootstrap-C_K2CKXC.mjs").then((n) => n.r);
1680
1680
  if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub client connect <server-url>` before onboarding.");
1681
1681
  if (!args.id) {
1682
1682
  args.id = await input({ message: "Agent ID:" });
@@ -1798,6 +1798,67 @@ function registerOnboardCommand(program) {
1798
1798
  });
1799
1799
  }
1800
1800
  //#endregion
1801
+ //#region src/commands/org.ts
1802
+ /**
1803
+ * `first-tree-hub org` — organization-level operations.
1804
+ *
1805
+ * Today this only ships `bind-tree`, called by Step 3 onboarding agents
1806
+ * after they create a fresh context-tree GitHub repo so the Hub records
1807
+ * the binding in the org's `context_tree` settings namespace. The verb
1808
+ * mirrors first-tree CLI's own `tree bind` vocabulary so agents reading
1809
+ * "bind-tree" know what it means without translation. See
1810
+ * docs/new-user-onboarding-design.md §7.4 (Path B).
1811
+ */
1812
+ function registerOrgCommands(program) {
1813
+ program.command("org").description("Organization-level operations").command("bind-tree").description("Bind the caller's organization to a context-tree GitHub URL").argument("<url>", "GitHub URL of the context-tree repository (https://github.com/...)").option("--org <orgId>", "Override the org to bind. Defaults to your selected/default org via /me.").action(async (rawUrl, options) => {
1814
+ try {
1815
+ const url = rawUrl.trim();
1816
+ if (!url) fail("INVALID_URL", "URL must not be empty", 2);
1817
+ try {
1818
+ const parsed = new URL(url);
1819
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") fail("INVALID_URL", `URL scheme must be http or https (got ${parsed.protocol})`, 2);
1820
+ } catch {
1821
+ fail("INVALID_URL", `"${url}" is not a valid URL`, 2);
1822
+ }
1823
+ const serverUrl = resolveServerUrl();
1824
+ const accessToken = await ensureFreshAccessToken();
1825
+ const orgId = options.org?.trim() || await resolveDefaultOrgId(serverUrl, accessToken);
1826
+ const res = await fetch(`${serverUrl}/api/v1/orgs/${encodeURIComponent(orgId)}/settings/context_tree`, {
1827
+ method: "PUT",
1828
+ headers: {
1829
+ Authorization: `Bearer ${accessToken}`,
1830
+ "Content-Type": "application/json"
1831
+ },
1832
+ body: JSON.stringify({ repo: url })
1833
+ });
1834
+ if (!res.ok) {
1835
+ const text = await res.text().catch(() => "");
1836
+ fail("PUT_FAILED", `hub returned ${res.status} on PUT /orgs/${orgId}/settings/context_tree: ${text.slice(0, 256)}`, 1);
1837
+ }
1838
+ print.status("•", `Bound organization to context-tree at ${url}`);
1839
+ success({
1840
+ orgId,
1841
+ repo: url
1842
+ });
1843
+ } catch (err) {
1844
+ fail("UNEXPECTED", err instanceof Error ? err.message : String(err), 1);
1845
+ }
1846
+ });
1847
+ }
1848
+ async function resolveDefaultOrgId(serverUrl, accessToken) {
1849
+ const res = await fetch(`${serverUrl}/api/v1/me`, {
1850
+ headers: { Authorization: `Bearer ${accessToken}` },
1851
+ signal: AbortSignal.timeout(1e4)
1852
+ });
1853
+ if (!res.ok) fail("ME_FAILED", `hub returned ${res.status} on /me`, 1);
1854
+ const me = await res.json();
1855
+ const memberships = me.memberships ?? [];
1856
+ if (me.defaultOrganizationId && memberships.some((m) => m.organizationId === me.defaultOrganizationId)) return me.defaultOrganizationId;
1857
+ if (memberships.length === 1 && memberships[0]) return memberships[0].organizationId;
1858
+ if (memberships.length === 0) fail("NO_ORG", "You don't belong to any organization", 1);
1859
+ fail("AMBIGUOUS_ORG", "Multiple organizations — pass --org <orgId> explicitly or set a default in the web UI first", 1);
1860
+ }
1861
+ //#endregion
1801
1862
  //#region src/commands/server.ts
1802
1863
  function registerServerCommands(program) {
1803
1864
  const server = program.command("server").description("Manage First Tree Hub server");
@@ -1990,6 +2051,7 @@ registerAgentCommands(program);
1990
2051
  registerConfigCommands(program);
1991
2052
  registerUpdateCommand(program);
1992
2053
  registerOnboardCommand(program);
2054
+ registerOrgCommands(program);
1993
2055
  program.parse();
1994
2056
  //#endregion
1995
2057
  export {};
@@ -0,0 +1,4 @@
1
+ import "./dist-CfvCT4E0.mjs";
2
+ import "./errors-CF5evtJt-B0NTIVPt.mjs";
3
+ import { y as listMyPinnedAgents } from "./client-DqdGiggm-NQoGZ2vM.mjs";
4
+ export { listMyPinnedAgents };
@@ -1,8 +1,8 @@
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";
1
+ import { w as clientCapabilitiesSchema } from "./dist-CfvCT4E0.mjs";
2
+ import { a as ConflictError, i as ClientUserMismatchError, l as organizations, n as BadRequestError, s as NotFoundError, u as users } from "./errors-CF5evtJt-B0NTIVPt.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
- //#region ../server/dist/client-CLdRbuml.mjs
5
+ //#region ../server/dist/client-DqdGiggm.mjs
6
6
  /**
7
7
  * Client connections. A client is a single SDK process (AgentRuntime) that may
8
8
  * host multiple agents. From the unified-user-token milestone on, a client is
@@ -137,195 +137,6 @@ z.object({
137
137
  connected: z.boolean(),
138
138
  lastActiveAt: z.string().nullable()
139
139
  });
140
- const presenceStatusSchema = z.enum(["online", "offline"]);
141
- const runtimeStateSchema = z.enum([
142
- "idle",
143
- "working",
144
- "blocked",
145
- "error"
146
- ]);
147
- z.enum([
148
- "active",
149
- "suspended",
150
- "evicted"
151
- ]);
152
- /** Wire-level states a client may report. `evicted` from a stale client is rejected. */
153
- const clientSessionStateSchema = z.enum(["active", "suspended"]);
154
- const sessionStateMessageSchema = z.object({
155
- chatId: z.string().min(1),
156
- state: clientSessionStateSchema
157
- });
158
- /** Client-reported runtime state override (client → server, per-agent). */
159
- const runtimeStateMessageSchema = z.object({ runtimeState: runtimeStateSchema });
160
- const agentBindRequestSchema = z.object({
161
- agentId: z.string().min(1),
162
- runtimeType: z.string().max(50),
163
- runtimeVersion: z.string().max(50).optional()
164
- });
165
- const AGENT_BIND_REJECT_REASONS = {
166
- WRONG_CLIENT: "wrong_client",
167
- NOT_OWNED: "not_owned",
168
- AGENT_SUSPENDED: "agent_suspended",
169
- WRONG_ORG: "wrong_org",
170
- UNKNOWN_AGENT: "unknown_agent",
171
- RUNTIME_PROVIDER_MISMATCH: "runtime_provider_mismatch"
172
- };
173
- z.enum([
174
- "wrong_client",
175
- "not_owned",
176
- "agent_suspended",
177
- "wrong_org",
178
- "unknown_agent",
179
- "runtime_provider_mismatch"
180
- ]);
181
- /** Header used on agent-scoped HTTP calls to select which managed agent the JWT acts as. */
182
- const AGENT_SELECTOR_HEADER = "x-agent-id";
183
- z.object({
184
- agentId: z.string(),
185
- status: presenceStatusSchema,
186
- connectedAt: z.string().nullable(),
187
- lastSeenAt: z.string(),
188
- clientId: z.string().nullable().optional(),
189
- runtimeType: z.string().nullable().optional(),
190
- runtimeVersion: z.string().nullable().optional(),
191
- runtimeState: runtimeStateSchema.nullable().optional(),
192
- activeSessions: z.number().int().nullable().optional(),
193
- totalSessions: z.number().int().nullable().optional(),
194
- runtimeUpdatedAt: z.string().nullable().optional()
195
- });
196
- z.object({
197
- total: z.number().int(),
198
- running: z.number().int(),
199
- byState: z.object({
200
- idle: z.number().int(),
201
- working: z.number().int(),
202
- blocked: z.number().int(),
203
- error: z.number().int()
204
- }),
205
- clients: z.number().int()
206
- });
207
- const runtimeProviderSchema = z.enum(["claude-code", "codex"]);
208
- const DEFAULT_RUNTIME_PROVIDER = "claude-code";
209
- const AGENT_TYPES = {
210
- HUMAN: "human",
211
- PERSONAL_ASSISTANT: "personal_assistant",
212
- AUTONOMOUS_AGENT: "autonomous_agent"
213
- };
214
- const agentTypeSchema = z.enum([
215
- "human",
216
- "personal_assistant",
217
- "autonomous_agent"
218
- ]);
219
- const AGENT_VISIBILITY = {
220
- PRIVATE: "private",
221
- ORGANIZATION: "organization"
222
- };
223
- const agentVisibilitySchema = z.enum(["private", "organization"]);
224
- const AGENT_STATUSES = {
225
- ACTIVE: "active",
226
- SUSPENDED: "suspended",
227
- DELETED: "deleted"
228
- };
229
- const AGENT_SOURCES = {
230
- ADMIN_API: "admin-api",
231
- PORTAL: "portal"
232
- };
233
- const agentSourceSchema = z.enum(["admin-api", "portal"]);
234
- z.enum(["active", "suspended"]);
235
- /**
236
- * Agent-name rules (see docs/agent-naming-design.md §3.1):
237
- * - Lowercase ASCII slug, hyphens + underscores allowed.
238
- * - Must start with alphanumeric: `-` / `_` as first char collide with
239
- * CLI flag parsing and markdown list syntax.
240
- * - 1–64 chars — aligned with `MENTION_REGEX` so any valid name can be
241
- * @-mentioned in chat. Older rows created under the previous 1–100
242
- * regex are grandfathered; the tight rule only gates new creates.
243
- */
244
- const AGENT_NAME_REGEX = /^[a-z0-9][a-z0-9_-]{0,63}$/;
245
- const RESERVED_AGENT_NAMES_SET = new Set([
246
- "admin",
247
- "agent",
248
- "first-tree",
249
- "hub",
250
- "me",
251
- "null",
252
- "system",
253
- "undefined"
254
- ]);
255
- function isReservedAgentName(name) {
256
- return RESERVED_AGENT_NAMES_SET.has(name);
257
- }
258
- const createAgentSchema = z.object({
259
- name: z.string().min(1).max(64).regex(AGENT_NAME_REGEX, "Must start with a letter or digit and contain only lowercase letters, digits, hyphens (-), and underscores (_). Max 64 chars.").refine((n) => !isReservedAgentName(n), { message: "That agent name is reserved — pick a different one." }).optional(),
260
- type: agentTypeSchema,
261
- displayName: z.string().min(1).max(200).optional(),
262
- delegateMention: z.string().max(100).optional(),
263
- organizationId: z.string().min(1).max(100).optional(),
264
- source: agentSourceSchema.optional(),
265
- visibility: agentVisibilitySchema.optional(),
266
- metadata: z.record(z.string(), z.unknown()).optional(),
267
- managerId: z.string().optional(),
268
- clientId: z.string().min(1).max(100).optional(),
269
- runtimeProvider: runtimeProviderSchema.optional()
270
- });
271
- const updateAgentSchema = z.object({
272
- type: agentTypeSchema.optional(),
273
- displayName: z.string().min(1).max(200).optional(),
274
- delegateMention: z.string().max(100).nullable().optional(),
275
- visibility: agentVisibilitySchema.optional(),
276
- metadata: z.record(z.string(), z.unknown()).optional(),
277
- managerId: z.string().nullable().optional(),
278
- clientId: z.string().min(1).max(100).nullable().optional()
279
- });
280
- /**
281
- * Service-level rebind input. Admin / owner re-binds an agent to a new
282
- * client and/or a new runtime provider in one atomic operation.
283
- *
284
- * `force` bypasses the capability-match check (e.g. when the client is
285
- * offline and capabilities are stale).
286
- */
287
- const rebindAgentSchema = z.object({
288
- clientId: z.string().min(1).max(100),
289
- runtimeProvider: runtimeProviderSchema,
290
- force: z.boolean().optional()
291
- });
292
- z.object({
293
- uuid: z.string(),
294
- name: z.string().nullable(),
295
- organizationId: z.string(),
296
- type: agentTypeSchema,
297
- displayName: z.string(),
298
- delegateMention: z.string().nullable(),
299
- inboxId: z.string(),
300
- status: z.string(),
301
- source: z.string().nullable().optional(),
302
- visibility: agentVisibilitySchema,
303
- metadata: z.record(z.string(), z.unknown()),
304
- managerId: z.string().nullable(),
305
- clientId: z.string().nullable(),
306
- runtimeProvider: runtimeProviderSchema,
307
- presenceStatus: presenceStatusSchema.optional(),
308
- createdAt: z.string(),
309
- updatedAt: z.string()
310
- });
311
- z.object({
312
- repo: z.string().nullable(),
313
- branch: z.string().nullable()
314
- });
315
- /**
316
- * Server → client WebSocket frame announcing that an agent has just been
317
- * pinned to the connected client (either created with `clientId` or bound via
318
- * PATCH NULL → ID). The client can auto-register a local config from this so
319
- * the operator doesn't have to run `first-tree-hub agent add` manually.
320
- */
321
- const agentPinnedMessageSchema = z.object({
322
- type: z.literal("agent:pinned"),
323
- agentId: z.string(),
324
- name: z.string().nullable(),
325
- displayName: z.string(),
326
- agentType: agentTypeSchema,
327
- runtimeProvider: runtimeProviderSchema
328
- });
329
140
  /**
330
141
  * Agent runtime configuration.
331
142
  *
@@ -500,6 +311,26 @@ const agentRuntimeConfigSchema = z.object({
500
311
  updatedBy: z.string()
501
312
  });
502
313
  /**
314
+ * Write-side shape with no `.default()` per field.
315
+ *
316
+ * `agentRuntimeConfigPayloadShape` carries `.default()` on every field for the
317
+ * read path (so legacy DB rows parse cleanly). On the PATCH side those defaults
318
+ * are actively harmful: Zod 4's `.partial()` makes a field optional but keeps
319
+ * the inner `ZodDefault`, so a body like `{ mcpServers: [...] }` parses to a
320
+ * fully-populated patch where the omitted fields are filled with their
321
+ * defaults — the service layer's `patch.x ?? current.x` then sees a truthy
322
+ * default and *replaces* the user's saved value with empty. Mirroring the 5
323
+ * fields here without defaults keeps "field absent" → `undefined` in the
324
+ * parsed patch, which is what the merge logic expects.
325
+ */
326
+ const agentRuntimeConfigPatchShape = z.object({
327
+ prompt: promptConfigSchema,
328
+ model: z.string(),
329
+ mcpServers: z.array(mcpServerSchema),
330
+ env: z.array(envEntrySchema),
331
+ gitRepos: z.array(gitRepoSchema)
332
+ }).partial();
333
+ /**
503
334
  * Patch payload for PATCH /api/v1/admin/agents/:uuid/config.
504
335
  *
505
336
  * - `expectedVersion` enforces optimistic locking; mismatch → 409.
@@ -507,9 +338,9 @@ const agentRuntimeConfigSchema = z.object({
507
338
  */
508
339
  const updateAgentRuntimeConfigSchema = z.object({
509
340
  expectedVersion: z.number().int().positive(),
510
- payload: agentRuntimeConfigPayloadShape.partial()
341
+ payload: agentRuntimeConfigPatchShape
511
342
  });
512
- const dryRunAgentRuntimeConfigSchema = z.object({ payload: agentRuntimeConfigPayloadShape.partial() });
343
+ const dryRunAgentRuntimeConfigSchema = z.object({ payload: agentRuntimeConfigPatchShape });
513
344
  z.object({
514
345
  current: agentRuntimeConfigSchema,
515
346
  next: agentRuntimeConfigPayloadSchema,
@@ -538,6 +369,196 @@ function deriveRepoLocalPath(url) {
538
369
  if (!trimmed) return "";
539
370
  return ((trimmed.split(/[?#]/)[0] ?? "").split(/[/:]/).filter(Boolean).pop() ?? "").replace(/\.git$/i, "");
540
371
  }
372
+ const presenceStatusSchema = z.enum(["online", "offline"]);
373
+ const runtimeStateSchema = z.enum([
374
+ "idle",
375
+ "working",
376
+ "blocked",
377
+ "error"
378
+ ]);
379
+ z.enum([
380
+ "active",
381
+ "suspended",
382
+ "evicted"
383
+ ]);
384
+ /** Wire-level states a client may report. `evicted` from a stale client is rejected. */
385
+ const clientSessionStateSchema = z.enum(["active", "suspended"]);
386
+ const sessionStateMessageSchema = z.object({
387
+ chatId: z.string().min(1),
388
+ state: clientSessionStateSchema
389
+ });
390
+ /** Client-reported runtime state override (client → server, per-agent). */
391
+ const runtimeStateMessageSchema = z.object({ runtimeState: runtimeStateSchema });
392
+ const agentBindRequestSchema = z.object({
393
+ agentId: z.string().min(1),
394
+ runtimeType: z.string().max(50),
395
+ runtimeVersion: z.string().max(50).optional()
396
+ });
397
+ const AGENT_BIND_REJECT_REASONS = {
398
+ WRONG_CLIENT: "wrong_client",
399
+ NOT_OWNED: "not_owned",
400
+ AGENT_SUSPENDED: "agent_suspended",
401
+ WRONG_ORG: "wrong_org",
402
+ UNKNOWN_AGENT: "unknown_agent",
403
+ RUNTIME_PROVIDER_MISMATCH: "runtime_provider_mismatch"
404
+ };
405
+ z.enum([
406
+ "wrong_client",
407
+ "not_owned",
408
+ "agent_suspended",
409
+ "wrong_org",
410
+ "unknown_agent",
411
+ "runtime_provider_mismatch"
412
+ ]);
413
+ /** Header used on agent-scoped HTTP calls to select which managed agent the JWT acts as. */
414
+ const AGENT_SELECTOR_HEADER = "x-agent-id";
415
+ z.object({
416
+ agentId: z.string(),
417
+ status: presenceStatusSchema,
418
+ connectedAt: z.string().nullable(),
419
+ lastSeenAt: z.string(),
420
+ clientId: z.string().nullable().optional(),
421
+ runtimeType: z.string().nullable().optional(),
422
+ runtimeVersion: z.string().nullable().optional(),
423
+ runtimeState: runtimeStateSchema.nullable().optional(),
424
+ activeSessions: z.number().int().nullable().optional(),
425
+ totalSessions: z.number().int().nullable().optional(),
426
+ runtimeUpdatedAt: z.string().nullable().optional()
427
+ });
428
+ z.object({
429
+ total: z.number().int(),
430
+ running: z.number().int(),
431
+ byState: z.object({
432
+ idle: z.number().int(),
433
+ working: z.number().int(),
434
+ blocked: z.number().int(),
435
+ error: z.number().int()
436
+ }),
437
+ clients: z.number().int()
438
+ });
439
+ const runtimeProviderSchema = z.enum(["claude-code", "codex"]);
440
+ const DEFAULT_RUNTIME_PROVIDER = "claude-code";
441
+ const AGENT_TYPES = {
442
+ HUMAN: "human",
443
+ PERSONAL_ASSISTANT: "personal_assistant",
444
+ AUTONOMOUS_AGENT: "autonomous_agent"
445
+ };
446
+ const agentTypeSchema = z.enum([
447
+ "human",
448
+ "personal_assistant",
449
+ "autonomous_agent"
450
+ ]);
451
+ const AGENT_VISIBILITY = {
452
+ PRIVATE: "private",
453
+ ORGANIZATION: "organization"
454
+ };
455
+ const agentVisibilitySchema = z.enum(["private", "organization"]);
456
+ const AGENT_STATUSES = {
457
+ ACTIVE: "active",
458
+ SUSPENDED: "suspended",
459
+ DELETED: "deleted"
460
+ };
461
+ const AGENT_SOURCES = {
462
+ ADMIN_API: "admin-api",
463
+ PORTAL: "portal"
464
+ };
465
+ const agentSourceSchema = z.enum(["admin-api", "portal"]);
466
+ z.enum(["active", "suspended"]);
467
+ /**
468
+ * Agent-name rules (see docs/agent-naming-design.md §3.1):
469
+ * - Lowercase ASCII slug, hyphens + underscores allowed.
470
+ * - Must start with alphanumeric: `-` / `_` as first char collide with
471
+ * CLI flag parsing and markdown list syntax.
472
+ * - 1–64 chars — aligned with `MENTION_REGEX` so any valid name can be
473
+ * @-mentioned in chat. Older rows created under the previous 1–100
474
+ * regex are grandfathered; the tight rule only gates new creates.
475
+ */
476
+ const AGENT_NAME_REGEX = /^[a-z0-9][a-z0-9_-]{0,63}$/;
477
+ const RESERVED_AGENT_NAMES_SET = new Set([
478
+ "admin",
479
+ "agent",
480
+ "first-tree",
481
+ "hub",
482
+ "me",
483
+ "null",
484
+ "system",
485
+ "undefined"
486
+ ]);
487
+ function isReservedAgentName(name) {
488
+ return RESERVED_AGENT_NAMES_SET.has(name);
489
+ }
490
+ const createAgentSchema = z.object({
491
+ name: z.string().min(1).max(64).regex(AGENT_NAME_REGEX, "Must start with a letter or digit and contain only lowercase letters, digits, hyphens (-), and underscores (_). Max 64 chars.").refine((n) => !isReservedAgentName(n), { message: "That agent name is reserved — pick a different one." }).optional(),
492
+ type: agentTypeSchema,
493
+ displayName: z.string().min(1).max(200).optional(),
494
+ delegateMention: z.string().max(100).optional(),
495
+ organizationId: z.string().min(1).max(100).optional(),
496
+ source: agentSourceSchema.optional(),
497
+ visibility: agentVisibilitySchema.optional(),
498
+ metadata: z.record(z.string(), z.unknown()).optional(),
499
+ managerId: z.string().optional(),
500
+ clientId: z.string().min(1).max(100).optional(),
501
+ runtimeProvider: runtimeProviderSchema.optional(),
502
+ gitRepos: z.array(gitRepoSchema).optional()
503
+ });
504
+ const updateAgentSchema = z.object({
505
+ type: agentTypeSchema.optional(),
506
+ displayName: z.string().min(1).max(200).optional(),
507
+ delegateMention: z.string().max(100).nullable().optional(),
508
+ visibility: agentVisibilitySchema.optional(),
509
+ metadata: z.record(z.string(), z.unknown()).optional(),
510
+ managerId: z.string().nullable().optional(),
511
+ clientId: z.string().min(1).max(100).nullable().optional()
512
+ });
513
+ /**
514
+ * Service-level rebind input. Admin / owner re-binds an agent to a new
515
+ * client and/or a new runtime provider in one atomic operation.
516
+ *
517
+ * `force` bypasses the capability-match check (e.g. when the client is
518
+ * offline and capabilities are stale).
519
+ */
520
+ const rebindAgentSchema = z.object({
521
+ clientId: z.string().min(1).max(100),
522
+ runtimeProvider: runtimeProviderSchema,
523
+ force: z.boolean().optional()
524
+ });
525
+ z.object({
526
+ uuid: z.string(),
527
+ name: z.string().nullable(),
528
+ organizationId: z.string(),
529
+ type: agentTypeSchema,
530
+ displayName: z.string(),
531
+ delegateMention: z.string().nullable(),
532
+ inboxId: z.string(),
533
+ status: z.string(),
534
+ source: z.string().nullable().optional(),
535
+ visibility: agentVisibilitySchema,
536
+ metadata: z.record(z.string(), z.unknown()),
537
+ managerId: z.string().nullable(),
538
+ clientId: z.string().nullable(),
539
+ runtimeProvider: runtimeProviderSchema,
540
+ presenceStatus: presenceStatusSchema.optional(),
541
+ createdAt: z.string(),
542
+ updatedAt: z.string()
543
+ });
544
+ z.object({
545
+ repo: z.string().nullable(),
546
+ branch: z.string().nullable()
547
+ });
548
+ /**
549
+ * Server → client WebSocket frame announcing that an agent has just been
550
+ * pinned to the connected client (either created with `clientId` or bound via
551
+ * PATCH NULL → ID). The client can auto-register a local config from this so
552
+ * the operator doesn't have to run `first-tree-hub agent add` manually.
553
+ */
554
+ const agentPinnedMessageSchema = z.object({
555
+ type: z.literal("agent:pinned"),
556
+ agentId: z.string(),
557
+ name: z.string().nullable(),
558
+ displayName: z.string(),
559
+ agentType: agentTypeSchema,
560
+ runtimeProvider: runtimeProviderSchema
561
+ });
541
562
  const loginSchema = z.object({
542
563
  username: z.string().min(1),
543
564
  password: z.string().min(1)
@@ -1054,6 +1075,43 @@ const createOrgFromMeSchema = z.object({
1054
1075
  name: z.string().min(2).max(50).regex(/^[a-z0-9][a-z0-9-]*$/),
1055
1076
  displayName: z.string().min(1).max(200)
1056
1077
  });
1078
+ /**
1079
+ * Body for `PATCH /me/onboarding`. v1 only mutates `dismissed` — true to
1080
+ * hide the onboarding stepper (server stamps `users.onboarding_dismissed_at
1081
+ * = NOW()`), false to restore it. See docs/new-user-onboarding-design.md
1082
+ * §8.4.
1083
+ */
1084
+ const patchOnboardingSchema = z.object({ dismissed: z.boolean().optional() });
1085
+ /**
1086
+ * Body for `POST /me/onboarding/events`. The web SPA reports key
1087
+ * milestones so the server can log them as a single funnel-trackable
1088
+ * stream alongside server-emitted events (`team_created`, `dismissed`).
1089
+ *
1090
+ * Server emits:
1091
+ * - `team_created` — at OAuth callback when joinPath === "solo"
1092
+ * - `dismissed` — when PATCH /me/onboarding flips dismissed
1093
+ *
1094
+ * Web reports:
1095
+ * - `team_renamed` — Step 1 user changed the auto-named team
1096
+ * - `agent_created` — Step 2 successfully created the agent
1097
+ * - `tree_chat_started` — Step 3 [Yes, set it up] succeeded
1098
+ * - `tree_intro_dismissed` — Step 3 [I'll do it later] clicked
1099
+ */
1100
+ const onboardingEventNameSchema = z.enum([
1101
+ "team_renamed",
1102
+ "agent_created",
1103
+ "tree_chat_started",
1104
+ "tree_intro_dismissed"
1105
+ ]);
1106
+ const onboardingEventSchema = z.object({
1107
+ event: onboardingEventNameSchema,
1108
+ attrs: z.record(z.string(), z.union([
1109
+ z.string(),
1110
+ z.number(),
1111
+ z.boolean(),
1112
+ z.null()
1113
+ ])).optional()
1114
+ });
1057
1115
  z.object({
1058
1116
  id: z.string(),
1059
1117
  organizationId: z.string(),
@@ -1159,12 +1217,26 @@ const githubDevCallbackQuerySchema = z.object({
1159
1217
  * 2. Add a key to `ORG_SETTINGS_NAMESPACES`.
1160
1218
  * 3. Done. No DB migration, no new API route.
1161
1219
  */
1220
+ const orgContextTreeRepoUrlSchema = z.string().url().refine((value) => {
1221
+ try {
1222
+ return new URL(value).protocol === "https:";
1223
+ } catch {
1224
+ return false;
1225
+ }
1226
+ }, { message: "Context Tree repo URL must use HTTPS." }).refine((value) => {
1227
+ try {
1228
+ const url = new URL(value);
1229
+ return url.username.length === 0 && url.password.length === 0;
1230
+ } catch {
1231
+ return false;
1232
+ }
1233
+ }, { message: "Context Tree repo URL must not include credentials." });
1162
1234
  const orgContextTreeStorageSchema = z.object({
1163
- repo: z.string().url().optional(),
1235
+ repo: orgContextTreeRepoUrlSchema.optional(),
1164
1236
  branch: z.string().default("main")
1165
1237
  });
1166
1238
  const orgContextTreeInputSchema = z.object({
1167
- repo: z.string().url().min(1).nullish(),
1239
+ repo: orgContextTreeRepoUrlSchema.nullish(),
1168
1240
  branch: z.string().min(1).nullish()
1169
1241
  });
1170
1242
  const orgContextTreeOutputSchema = z.object({
@@ -1521,4 +1593,4 @@ z.object({
1521
1593
  capabilities: serverCapabilitiesSchema.optional()
1522
1594
  }).passthrough();
1523
1595
  //#endregion
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 };
1596
+ export { loginSchema as $, createAgentSchema as A, githubCallbackQuerySchema as B, agentTypeSchema as C, updateChatSchema as Ct, contextTreeSnapshotSchema as D, updateTaskStatusSchema as Dt, connectTokenExchangeSchema as E, updateOrganizationSchema 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, wsAuthFrameSchema as Ot, createOrgFromMeSchema as P, listMeChatsQuerySchema as Q, dryRunAgentRuntimeConfigSchema as R, agentRuntimeConfigPayloadSchema as S, updateAgentSchema as St, clientRegisterSchema as T, updateMemberSchema as Tt, imageInlineContentSchema as U, githubDevCallbackQuerySchema as V, inboxAckFrameSchema as W, joinByInvitationSchema as X, isReservedAgentName as Y, linkTaskChatSchema as Z, addParticipantSchema as _, sessionStateMessageSchema as _t, AGENT_STATUSES as a, rebindAgentSchema as at, agentBindRequestSchema as b, updateAdapterConfigSchema as bt, DEFAULT_RUNTIME_PROVIDER as c, safeRedirectPath as ct, TASK_CREATOR_TYPES as d, sendMessageSchema as dt, messageSourceSchema as et, TASK_HEALTH_SIGNALS as f, sendToAgentSchema as ft, addMeChatParticipantsSchema as g, sessionReconcileRequestSchema as gt, WS_AUTH_FRAME_TIMEOUT_MS as h, sessionEventSchema as ht, AGENT_SOURCES as i, patchOnboardingSchema as it, createChatSchema as j, createAdapterMappingSchema as k, MENTION_REGEX as l, scanMentionTokens as lt, TASK_TERMINAL_STATUSES as m, sessionEventMessageSchema as mt, AGENT_NAME_REGEX as n, onboardingEventSchema as nt, AGENT_TYPES as o, refreshTokenSchema as ot, TASK_STATUSES as p, sessionCompletionMessageSchema as pt, isOrgSettingNamespace as q, AGENT_SELECTOR_HEADER as r, paginationQuerySchema as rt, AGENT_VISIBILITY as s, runtimeStateMessageSchema as st, AGENT_BIND_REJECT_REASONS as t, notificationQuerySchema as tt, ORG_SETTINGS_NAMESPACES as u, selfServiceFeishuBotSchema as ut, adminCreateTaskSchema as v, stripCode as vt, clientCapabilitiesSchema as w, updateClientCapabilitiesSchema as wt, agentPinnedMessageSchema as x, updateAgentRuntimeConfigSchema as xt, adminUpdateTaskSchema as y, taskListQuerySchema as yt, extractMentions as z };
@@ -0,0 +1,13 @@
1
+ -- Onboarding stepper dismissal flag. Decoupled from the server-side
2
+ -- `onboardingStep` enum so the stepper keeps rendering across all three
3
+ -- UI steps (server-side onboardingStep flips to `completed` at the end of
4
+ -- Step 2; Step 3 is purely client-driven and the stepper must keep
5
+ -- showing during the tree-init chat).
6
+ --
7
+ -- See docs/new-user-onboarding-design.md §8.
8
+ --
9
+ -- NULL → stepper renders
10
+ -- value → user clicked the `✕`; stepper unmounts. Irreversible from UI v1.
11
+
12
+ ALTER TABLE "users"
13
+ ADD COLUMN IF NOT EXISTS "onboarding_dismissed_at" timestamp with time zone;