@agent-team-foundation/first-tree-hub 0.4.0 → 0.6.0

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,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { A as createAdminUser, C as printResults, F as cleanWorkspaces, M as FirstTreeHubSDK, N as SdkError, O as stopPostgres, P as SessionRegistry, S as checkWebSocket, _ as checkGitHubToken, a as formatCheckReport, b as checkServerHealth, c as onboardCreate, d as checkAgentConfigs, f as checkAgentTokens, g as checkDocker, h as checkDatabase, i as promptMissingFields, k as ClientRuntime, l as saveOnboardState, m as checkContextTreeRepo, n as isInteractive, o as loadOnboardState, p as checkClientConfig, r as promptAddAgent, s as onboardCheck, t as startServer, u as runMigrations, v as checkNodeVersion, x as checkServerReachable, y as checkServerConfig } from "../core-CMeOAZmx.mjs";
3
- import { S as setConfigValue, _ as readConfigFile, c as DEFAULT_CONFIG_DIR, d as agentConfigSchema, f as clientConfigSchema, g as loadAgents, h as initConfig, l as DEFAULT_DATA_DIR, m as getConfigValue, o as resolveAgentToken, s as resolveServerUrl, t as bootstrapToken, u as DEFAULT_HOME_DIR, v as resetConfig, x as serverConfigSchema, y as resetConfigMeta } from "../bootstrap-uyPaaI05.mjs";
2
+ import { C as resetConfigMeta, E as setConfigValue, S as resetConfig, T as serverConfigSchema, b as loadAgents, c as resolveAgentToken, d as saveCredentials, f as DEFAULT_CONFIG_DIR, g as clientConfigSchema, h as agentConfigSchema, i as ensureFreshAdminToken, l as resolveServerUrl, m as DEFAULT_HOME_DIR, p as DEFAULT_DATA_DIR, s as maskToken, t as bootstrapToken, u as saveAgentConfig, v as getConfigValue, x as readConfigFile, y as initConfig } from "../bootstrap-BnlTKa0H.mjs";
3
+ import { A as FirstTreeHubSDK, D as ClientRuntime, E as stopPostgres, M as SessionRegistry, N as cleanWorkspaces, O as createOwner, _ as checkServerConfig, a as formatCheckReport, b as checkWebSocket, c as onboardCreate, d as checkAgentConfigs, f as checkAgentTokens, g as checkNodeVersion, h as checkDocker, i as promptMissingFields, j as SdkError, l as saveOnboardState, m as checkDatabase, n as isInteractive, o as loadOnboardState, p as checkClientConfig, r as promptAddAgent, s as onboardCheck, t as startServer, u as runMigrations, v as checkServerHealth, x as printResults, y as checkServerReachable } from "../core-B9bH7EjM.mjs";
4
4
  import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-Y4m2zFc3.mjs";
5
5
  import { createRequire } from "node:module";
6
6
  import { Command } from "commander";
7
7
  import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
8
8
  import { join } from "node:path";
9
- import { confirm, input, select } from "@inquirer/prompts";
9
+ import { confirm, input, password, select } from "@inquirer/prompts";
10
10
  //#region src/cli/output.ts
11
11
  /** Write a success JSON envelope to stdout. */
12
12
  function success(data) {
@@ -78,6 +78,20 @@ function readStdin() {
78
78
  process.stdin.on("error", reject);
79
79
  });
80
80
  }
81
+ /**
82
+ * Resolve an agent name (or UUID) to its record via the admin agents API.
83
+ * Accepts either a name or a UUID; throws via fail() if not found.
84
+ */
85
+ async function resolveAgent(serverUrl, adminToken, agentName) {
86
+ const res = await fetch(`${serverUrl}/api/v1/admin/agents?limit=100`, {
87
+ headers: { Authorization: `Bearer ${adminToken}` },
88
+ signal: AbortSignal.timeout(1e4)
89
+ });
90
+ if (!res.ok) fail("FETCH_ERROR", `Failed to list agents: ${res.status}`, 1);
91
+ const found = (await res.json()).items.find((a) => a.name === agentName || a.uuid === agentName);
92
+ if (!found) fail("NOT_FOUND", `Agent "${agentName}" not found`, 1);
93
+ return found;
94
+ }
81
95
  function registerAgentCommands(program) {
82
96
  const agent = program.command("agent").description("Agent management — config, tokens, bindings, messaging");
83
97
  agent.command("add [name]").description("Add an agent instance").option("-t, --token <token>", "Agent token").action(async (name, options) => {
@@ -135,14 +149,46 @@ function registerAgentCommands(program) {
135
149
  process.stderr.write(" No agents configured.\n");
136
150
  return;
137
151
  }
138
- for (const [name, config] of agents) {
139
- const masked = config.token.length > 8 ? `${config.token.slice(0, 6)}***${config.token.slice(-2)}` : "***";
140
- process.stderr.write(` ${name.padEnd(20)} type: ${config.type.padEnd(14)} token: ${masked}\n`);
141
- }
152
+ for (const [name, config] of agents) process.stderr.write(` ${name.padEnd(20)} runtime: ${config.runtime.padEnd(14)} token: ${maskToken(config.token)}\n`);
142
153
  } catch {
143
154
  process.stderr.write(" No agents configured.\n");
144
155
  }
145
156
  });
157
+ agent.command("create <name>").description("Create an agent on Hub and bind it locally (CLI-first)").requiredOption("--type <type>", "Agent type (human, personal_assistant, autonomous_agent)").option("--runtime <runtime>", "Runtime handler (default: claude-code)", "claude-code").option("--display-name <name>", "Display name").option("--server <url>", "Hub server URL").action(async (name, options) => {
158
+ try {
159
+ const serverUrl = resolveServerUrl(options.server);
160
+ const headers = {
161
+ Authorization: `Bearer ${await ensureFreshAdminToken()}`,
162
+ "Content-Type": "application/json"
163
+ };
164
+ const createBody = {
165
+ name,
166
+ type: options.type
167
+ };
168
+ if (options.displayName) createBody.displayName = options.displayName;
169
+ const createRes = await fetch(`${serverUrl}/api/v1/admin/agents`, {
170
+ method: "POST",
171
+ headers,
172
+ body: JSON.stringify(createBody),
173
+ signal: AbortSignal.timeout(1e4)
174
+ });
175
+ if (!createRes.ok) fail("CREATE_ERROR", (await createRes.json().catch(() => ({}))).error ?? `Failed to create agent (HTTP ${createRes.status})`, 1);
176
+ const agent = await createRes.json();
177
+ process.stderr.write(` \u2713 Agent created: ${agent.name ?? agent.uuid}\n`);
178
+ const tokenRes = await fetch(`${serverUrl}/api/v1/admin/agents/${agent.uuid}/tokens`, {
179
+ method: "POST",
180
+ headers,
181
+ body: JSON.stringify({ name: "default" }),
182
+ signal: AbortSignal.timeout(1e4)
183
+ });
184
+ if (!tokenRes.ok) fail("TOKEN_ERROR", `Failed to generate token (HTTP ${tokenRes.status})`, 1);
185
+ const agentDir = saveAgentConfig(name, (await tokenRes.json()).token, options.runtime);
186
+ process.stderr.write(` \u2713 Token saved: ${agentDir}/agent.yaml\n`);
187
+ process.stderr.write(` \u2713 Agent ready — running client will auto-bind\n`);
188
+ } catch (error) {
189
+ fail("CREATE_ERROR", error instanceof Error ? error.message : String(error));
190
+ }
191
+ });
146
192
  agent.command("workspace").description("Manage agent workspaces").command("clean [agent-name]").description("Remove stale workspace directories (older than TTL with no active session)").option("--ttl <days>", "TTL in days", String(DEFAULT_WORKSPACE_TTL_MS / (1440 * 60 * 1e3))).action((agentName, options) => {
147
193
  const defaultDays = DEFAULT_WORKSPACE_TTL_MS / (1440 * 60 * 1e3);
148
194
  const ttlMs = Number.parseInt(options?.ttl ?? String(defaultDays), 10) * 24 * 60 * 60 * 1e3;
@@ -165,10 +211,10 @@ function registerAgentCommands(program) {
165
211
  }
166
212
  process.stderr.write(` ${totalRemoved} workspace(s) cleaned.\n`);
167
213
  });
168
- agent.command("token").description("Agent token management").command("bootstrap <agentId>").description("Bootstrap a token using GitHub identity (requires gh CLI)").option("--save-to <target>", "Save token to: \"agent\" (default) or a file path", "agent").option("--server <url>", "Hub server URL").action(async (agentId, options) => {
214
+ agent.command("token").description("Agent token management").command("bootstrap <agentName>").description("Bootstrap a token using GitHub identity (requires gh CLI)").option("--save-to <target>", "Save token to: \"agent\" (default) or a file path", "agent").option("--server <url>", "Hub server URL").action(async (agentName, options) => {
169
215
  try {
170
- const result = await bootstrapToken(resolveServerUrl(options.server), agentId, { saveTo: options.saveTo });
171
- if (options.saveTo === "agent") process.stderr.write(`Token saved to ${DEFAULT_HOME_DIR}/config/agents/${agentId}/agent.yaml\n`);
216
+ const result = await bootstrapToken(resolveServerUrl(options.server), agentName, { saveTo: options.saveTo });
217
+ if (options.saveTo === "agent") process.stderr.write(`Token saved to ${DEFAULT_HOME_DIR}/config/agents/${agentName}/agent.yaml\n`);
172
218
  else process.stderr.write(`Token saved to ${options.saveTo}\n`);
173
219
  success({
174
220
  agentId: result.agentId,
@@ -258,6 +304,214 @@ function registerAgentCommands(program) {
258
304
  handleSdkError(error);
259
305
  }
260
306
  });
307
+ agent.command("status [name]").description("Show agent runtime status from Hub server").option("--server <url>", "Hub server URL").action(async (name, options) => {
308
+ try {
309
+ const serverUrl = resolveServerUrl(options?.server);
310
+ const response = await fetch(`${serverUrl}/api/v1/admin/agents/activity`, {
311
+ headers: { Authorization: `Bearer ${await ensureFreshAdminToken()}` },
312
+ signal: AbortSignal.timeout(1e4)
313
+ });
314
+ if (!response.ok) fail("FETCH_ERROR", `Server returned ${response.status}`, 1);
315
+ const data = await response.json();
316
+ if (name) {
317
+ const agent = data.agents.find((a) => a.agentId === name);
318
+ if (!agent) {
319
+ process.stderr.write(`\n Agent "${name}" is not running.\n\n`);
320
+ return;
321
+ }
322
+ process.stderr.write(`\n Agent: ${agent.agentId}\n`);
323
+ process.stderr.write(` Runtime: ${agent.runtimeType ?? "—"}\n`);
324
+ process.stderr.write(` State: ${agent.runtimeState ?? "—"}\n`);
325
+ if (agent.activeSessions !== null) process.stderr.write(` Sessions: ${agent.activeSessions} active / ${agent.totalSessions ?? 0} total\n`);
326
+ if (agent.clientId) process.stderr.write(` Client: ${agent.clientId}\n`);
327
+ process.stderr.write("\n");
328
+ return;
329
+ }
330
+ process.stderr.write(`\n Hub: ${serverUrl}\n\n`);
331
+ process.stderr.write(` Clients: ${data.clients} connected\n`);
332
+ process.stderr.write(` Agents: ${data.running} running / ${data.total} total\n`);
333
+ process.stderr.write(` Errors: ${data.byState.error} | Blocked: ${data.byState.blocked} | Working: ${data.byState.working} | Idle: ${data.byState.idle}\n\n`);
334
+ if (data.agents.length > 0) {
335
+ const header = ` ${"AGENT".padEnd(18)} ${"RUNTIME".padEnd(14)} ${"STATE".padEnd(10)} SESSIONS`;
336
+ process.stderr.write(`${header}\n`);
337
+ process.stderr.write(` ${"─".repeat(header.length - 2)}\n`);
338
+ for (const a of data.agents) {
339
+ const sessions = a.activeSessions !== null ? `${a.activeSessions}/${a.totalSessions ?? 0}` : "—";
340
+ process.stderr.write(` ${(a.agentId ?? "").padEnd(18)} ${(a.runtimeType ?? "—").padEnd(14)} ${(a.runtimeState ?? "—").padEnd(10)} ${sessions}\n`);
341
+ }
342
+ process.stderr.write("\n");
343
+ }
344
+ } catch (error) {
345
+ if (error instanceof Error && error.message.includes("ADMIN_TOKEN")) {
346
+ try {
347
+ const me = await createSdk().register();
348
+ process.stderr.write(`\n Agent: ${me.agentId} (${me.displayName ?? "no name"})\n`);
349
+ process.stderr.write(` Type: ${me.type}\n`);
350
+ process.stderr.write(` Status: ${me.status}\n\n`);
351
+ } catch (sdkError) {
352
+ handleSdkError(sdkError);
353
+ }
354
+ return;
355
+ }
356
+ fail("STATUS_ERROR", error instanceof Error ? error.message : String(error));
357
+ }
358
+ });
359
+ agent.command("reset <name>").description("Reset agent error state to idle").option("--server <url>", "Hub server URL").action(async (name, options) => {
360
+ try {
361
+ const serverUrl = resolveServerUrl(options.server);
362
+ const response = await fetch(`${serverUrl}/api/v1/admin/agents/activity/${name}/reset-activity`, {
363
+ method: "POST",
364
+ headers: { Authorization: `Bearer ${await ensureFreshAdminToken()}` },
365
+ signal: AbortSignal.timeout(1e4)
366
+ });
367
+ if (!response.ok) fail("RESET_ERROR", `Server returned ${response.status}`, 1);
368
+ process.stderr.write(` Agent "${name}" reset to idle.\n`);
369
+ } catch (error) {
370
+ fail("RESET_ERROR", error instanceof Error ? error.message : String(error));
371
+ }
372
+ });
373
+ agent.command("sessions <agent-name>").description("List sessions for an agent").option("--server <url>", "Hub server URL").option("--state <state>", "Filter by session state (active/suspended/evicted)").action(async (agentName, options) => {
374
+ try {
375
+ const serverUrl = resolveServerUrl(options.server);
376
+ const adminToken = await ensureFreshAdminToken();
377
+ const agentId = (await resolveAgent(serverUrl, adminToken, agentName)).uuid;
378
+ const qs = options.state ? `?state=${options.state}` : "";
379
+ const response = await fetch(`${serverUrl}/api/v1/admin/sessions/agents/${agentId}${qs}`, {
380
+ headers: { Authorization: `Bearer ${adminToken}` },
381
+ signal: AbortSignal.timeout(1e4)
382
+ });
383
+ if (!response.ok) fail("FETCH_ERROR", `Server returned ${response.status}`, 1);
384
+ const sessions = await response.json();
385
+ if (sessions.length === 0) {
386
+ process.stderr.write(`\n No sessions for "${agentName}".\n\n`);
387
+ return;
388
+ }
389
+ process.stderr.write(`\n Sessions for "${agentName}":\n\n`);
390
+ const header = ` ${"CHAT".padEnd(40)} ${"STATE".padEnd(12)} ${"RUNTIME".padEnd(10)} LAST ACTIVITY`;
391
+ process.stderr.write(`${header}\n`);
392
+ process.stderr.write(` ${"─".repeat(header.length - 2)}\n`);
393
+ for (const s of sessions) {
394
+ const chatShort = s.chatId.length > 38 ? `${s.chatId.slice(0, 35)}...` : s.chatId;
395
+ process.stderr.write(` ${chatShort.padEnd(40)} ${s.state.padEnd(12)} ${(s.runtimeState ?? "—").padEnd(10)} ${s.lastActivityAt}\n`);
396
+ }
397
+ process.stderr.write("\n");
398
+ } catch (error) {
399
+ fail("SESSIONS_ERROR", error instanceof Error ? error.message : String(error));
400
+ }
401
+ });
402
+ const sessionCmd = agent.command("session").description("Session lifecycle commands");
403
+ for (const [cmd, desc] of [
404
+ ["suspend", "Suspend a session"],
405
+ ["resume", "Resume a suspended session"],
406
+ ["terminate", "Terminate a session"]
407
+ ]) sessionCmd.command(`${cmd} <agent-name> <chat-id>`).description(desc).option("--server <url>", "Hub server URL").action(async (agentName, chatId, options) => {
408
+ try {
409
+ const serverUrl = resolveServerUrl(options.server);
410
+ const adminToken = await ensureFreshAdminToken();
411
+ const agentId = (await resolveAgent(serverUrl, adminToken, agentName)).uuid;
412
+ const response = await fetch(`${serverUrl}/api/v1/admin/sessions/agents/${agentId}/${chatId}/${cmd}`, {
413
+ method: "POST",
414
+ headers: { Authorization: `Bearer ${adminToken}` },
415
+ signal: AbortSignal.timeout(1e4)
416
+ });
417
+ if (!response.ok) {
418
+ const body = await response.text();
419
+ fail("SESSION_CMD_ERROR", `Server returned ${response.status}: ${body}`, 1);
420
+ }
421
+ process.stderr.write(` Session ${cmd}: ${chatId} → sent\n`);
422
+ } catch (error) {
423
+ fail("SESSION_CMD_ERROR", error instanceof Error ? error.message : String(error));
424
+ }
425
+ });
426
+ agent.command("chat <agent-name>").description("Open an interactive chat with an agent (as admin human agent)").option("--server <url>", "Hub server URL").action(async (agentName, options) => {
427
+ try {
428
+ const serverUrl = resolveServerUrl(options.server);
429
+ const adminToken = await ensureFreshAdminToken();
430
+ const headers = {
431
+ Authorization: `Bearer ${adminToken}`,
432
+ "Content-Type": "application/json"
433
+ };
434
+ const targetAgent = await resolveAgent(serverUrl, adminToken, agentName);
435
+ const dmRes = await fetch(`${serverUrl}/api/v1/admin/agents/${targetAgent.uuid}/chats`, {
436
+ method: "POST",
437
+ headers,
438
+ signal: AbortSignal.timeout(1e4)
439
+ });
440
+ if (!dmRes.ok) {
441
+ const body = await dmRes.text();
442
+ fail("DM_ERROR", `Failed to create DM: ${dmRes.status} — ${body}`, 1);
443
+ }
444
+ const dm = await dmRes.json();
445
+ process.stderr.write(`\n Chat with ${targetAgent.displayName ?? targetAgent.name ?? targetAgent.uuid}\n`);
446
+ process.stderr.write(` Chat ID: ${dm.id}\n`);
447
+ process.stderr.write(` Type a message and press Enter. Ctrl+C to exit.\n\n`);
448
+ const rl = (await import("node:readline")).createInterface({
449
+ input: process.stdin,
450
+ output: process.stderr,
451
+ prompt: " > "
452
+ });
453
+ let lastSeenAt = null;
454
+ const pollMessages = async () => {
455
+ try {
456
+ const qs = lastSeenAt ? `?limit=50` : `?limit=10`;
457
+ const msgRes = await fetch(`${serverUrl}/api/v1/admin/chats/${dm.id}/messages${qs}`, {
458
+ headers,
459
+ signal: AbortSignal.timeout(1e4)
460
+ });
461
+ if (!msgRes.ok) return;
462
+ const msgData = await msgRes.json();
463
+ const cutoff = lastSeenAt;
464
+ const newMessages = cutoff ? msgData.items.filter((m) => m.createdAt > cutoff && m.senderId === targetAgent.uuid).reverse() : [];
465
+ for (const msg of newMessages) {
466
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
467
+ const preview = content.length > 500 ? `${content.slice(0, 500)}...` : content;
468
+ process.stderr.write(`\r [${targetAgent.displayName ?? targetAgent.name ?? "agent"}] ${preview}\n`);
469
+ }
470
+ if (msgData.items.length > 0 && msgData.items[0]) {
471
+ const newest = msgData.items[0].createdAt;
472
+ if (!lastSeenAt || newest > lastSeenAt) lastSeenAt = newest;
473
+ }
474
+ } catch {}
475
+ };
476
+ await pollMessages();
477
+ const pollTimer = setInterval(() => {
478
+ pollMessages().then(() => rl.prompt());
479
+ }, 2e3);
480
+ rl.prompt();
481
+ rl.on("line", async (line) => {
482
+ const text = line.trim();
483
+ if (!text) {
484
+ rl.prompt();
485
+ return;
486
+ }
487
+ try {
488
+ const sendRes = await fetch(`${serverUrl}/api/v1/admin/chats/${dm.id}/messages`, {
489
+ method: "POST",
490
+ headers,
491
+ body: JSON.stringify({
492
+ format: "text",
493
+ content: text
494
+ }),
495
+ signal: AbortSignal.timeout(1e4)
496
+ });
497
+ if (!sendRes.ok) {
498
+ const body = await sendRes.text();
499
+ process.stderr.write(` [error] Failed to send: ${sendRes.status} — ${body}\n`);
500
+ } else lastSeenAt = (await sendRes.json()).createdAt;
501
+ } catch (err) {
502
+ process.stderr.write(` [error] ${err instanceof Error ? err.message : String(err)}\n`);
503
+ }
504
+ rl.prompt();
505
+ });
506
+ rl.on("close", () => {
507
+ clearInterval(pollTimer);
508
+ process.stderr.write("\n Chat ended.\n");
509
+ process.exit(0);
510
+ });
511
+ } catch (error) {
512
+ fail("CHAT_ERROR", error instanceof Error ? error.message : String(error));
513
+ }
514
+ });
261
515
  agent.command("register").description("Register this agent and return identity info").action(async () => {
262
516
  try {
263
517
  success(await createSdk().register());
@@ -292,21 +546,19 @@ function registerClientCommands(program) {
292
546
  schema: clientConfigSchema,
293
547
  role: "client"
294
548
  });
549
+ const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
295
550
  const agents = loadAgents({
296
551
  schema: agentConfigSchema,
297
- agentsDir: join(DEFAULT_CONFIG_DIR, "agents")
552
+ agentsDir
298
553
  });
299
- if (agents.size === 0) {
300
- process.stderr.write(" No agents configured.\n");
301
- process.stderr.write(" Add one with: first-tree-hub agent add <name> --token <token>\n");
302
- process.exit(1);
303
- }
304
554
  process.stderr.write(`\n Connecting to ${config.server.url}...\n`);
305
555
  const runtime = new ClientRuntime(config.server.url);
306
556
  for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
307
557
  await runtime.start();
558
+ runtime.watchAgentsDir(agentsDir);
308
559
  const shutdown = async () => {
309
560
  process.stderr.write("\n Shutting down...\n");
561
+ runtime.unwatchAgentsDir();
310
562
  await runtime.stop();
311
563
  process.exit(0);
312
564
  };
@@ -349,15 +601,64 @@ function registerClientCommands(program) {
349
601
  return;
350
602
  }
351
603
  process.stderr.write("\n Configured agents:\n\n");
352
- for (const [name, config] of agents) {
353
- const masked = config.token.length > 8 ? `${config.token.slice(0, 6)}***${config.token.slice(-2)}` : "***";
354
- process.stderr.write(` ${name.padEnd(20)} type: ${config.type.padEnd(14)} token: ${masked}\n`);
355
- }
604
+ for (const [name, config] of agents) process.stderr.write(` ${name.padEnd(20)} runtime: ${config.runtime.padEnd(14)} token: ${maskToken(config.token)}\n`);
356
605
  process.stderr.write("\n");
357
606
  } catch {
358
607
  process.stderr.write(" No agents directory found.\n");
359
608
  }
360
609
  });
610
+ client.command("hub-list").description("List connected clients on the Hub server").option("--server <url>", "Hub server URL").action(async (options) => {
611
+ try {
612
+ const serverUrl = resolveServerUrl(options.server);
613
+ const token = await ensureFreshAdminToken();
614
+ const response = await fetch(`${serverUrl}/api/v1/admin/clients`, {
615
+ headers: { Authorization: `Bearer ${token}` },
616
+ signal: AbortSignal.timeout(1e4)
617
+ });
618
+ if (!response.ok) fail("FETCH_ERROR", `Server returned ${response.status}`, 1);
619
+ const clients = await response.json();
620
+ if (clients.length === 0) {
621
+ process.stderr.write(" No connected clients.\n");
622
+ return;
623
+ }
624
+ process.stderr.write(`\n Connected Clients: ${clients.length}\n\n`);
625
+ const header = ` ${"CLIENT".padEnd(20)} ${"HOST".padEnd(25)} ${"AGENTS".padEnd(8)} CONNECTED`;
626
+ process.stderr.write(`${header}\n`);
627
+ process.stderr.write(` ${"─".repeat(header.length - 2)}\n`);
628
+ for (const c of clients) {
629
+ const since = c.connectedAt ? timeSince(c.connectedAt) : "—";
630
+ process.stderr.write(` ${c.id.padEnd(20)} ${(c.hostname ?? "—").padEnd(25)} ${String(c.agentCount).padEnd(8)} ${since}\n`);
631
+ }
632
+ process.stderr.write("\n");
633
+ } catch (error) {
634
+ fail("CLIENT_LIST_ERROR", error instanceof Error ? error.message : String(error));
635
+ }
636
+ });
637
+ client.command("hub-disconnect <clientId>").description("Force-disconnect a client from the Hub server").option("--server <url>", "Hub server URL").action(async (clientId, options) => {
638
+ try {
639
+ const serverUrl = resolveServerUrl(options.server);
640
+ const token = await ensureFreshAdminToken();
641
+ const response = await fetch(`${serverUrl}/api/v1/admin/clients/${clientId}/disconnect`, {
642
+ method: "POST",
643
+ headers: { Authorization: `Bearer ${token}` },
644
+ signal: AbortSignal.timeout(1e4)
645
+ });
646
+ if (!response.ok) fail("DISCONNECT_ERROR", `Server returned ${response.status}`, 1);
647
+ process.stderr.write(` Client "${clientId}" disconnected.\n`);
648
+ } catch (error) {
649
+ fail("DISCONNECT_ERROR", error instanceof Error ? error.message : String(error));
650
+ }
651
+ });
652
+ }
653
+ function timeSince(isoDate) {
654
+ const ms = Date.now() - new Date(isoDate).getTime();
655
+ const seconds = Math.floor(ms / 1e3);
656
+ if (seconds < 60) return `${seconds}s`;
657
+ const minutes = Math.floor(seconds / 60);
658
+ if (minutes < 60) return `${minutes}m`;
659
+ const hours = Math.floor(minutes / 60);
660
+ if (hours < 24) return `${hours}h ${minutes % 60}m`;
661
+ return `${Math.floor(hours / 24)}d ${hours % 24}h`;
361
662
  }
362
663
  //#endregion
363
664
  //#region src/commands/config.ts
@@ -453,16 +754,100 @@ function isSecretField(schema, dotPath) {
453
754
  return false;
454
755
  }
455
756
  //#endregion
757
+ //#region src/commands/connect.ts
758
+ /**
759
+ * Authenticate via connect token — exchange for full JWT credentials.
760
+ */
761
+ async function authenticateWithToken(url, token) {
762
+ const res = await fetch(`${url}/api/v1/auth/connect-token`, {
763
+ method: "POST",
764
+ headers: { "Content-Type": "application/json" },
765
+ body: JSON.stringify({ token }),
766
+ signal: AbortSignal.timeout(1e4)
767
+ });
768
+ if (!res.ok) fail("AUTH_ERROR", (await res.json().catch(() => ({}))).error ?? `Token exchange failed (HTTP ${res.status})`, 1);
769
+ return await res.json();
770
+ }
771
+ /**
772
+ * Authenticate via interactive username/password login.
773
+ */
774
+ async function authenticateInteractive(url) {
775
+ process.stderr.write("\n Log in to Hub:\n");
776
+ const username = await input({ message: " Username:" });
777
+ const pw = await password({ message: " Password:" });
778
+ const loginRes = await fetch(`${url}/api/v1/auth/login`, {
779
+ method: "POST",
780
+ headers: { "Content-Type": "application/json" },
781
+ body: JSON.stringify({
782
+ username,
783
+ password: pw
784
+ }),
785
+ signal: AbortSignal.timeout(1e4)
786
+ });
787
+ if (!loginRes.ok) fail("AUTH_ERROR", (await loginRes.json().catch(() => ({}))).error ?? `Login failed (HTTP ${loginRes.status})`, 1);
788
+ return await loginRes.json();
789
+ }
790
+ function registerConnectCommand(program) {
791
+ program.command("connect <server-url>").description("Connect to a Hub server — configure, authenticate, and start client").option("--token <token>", "Connect token (from Hub web console) — skips interactive login").action(async (serverUrl, options) => {
792
+ try {
793
+ const url = serverUrl.replace(/\/+$/, "");
794
+ setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", url);
795
+ process.stderr.write(`\n \u2713 Server configured: ${url}\n`);
796
+ saveCredentials({
797
+ ...options.token ? await authenticateWithToken(url, options.token) : await authenticateInteractive(url),
798
+ serverUrl: url
799
+ });
800
+ process.stderr.write(" ✓ Authenticated\n");
801
+ resetConfig();
802
+ resetConfigMeta();
803
+ const config = await initConfig({
804
+ schema: clientConfigSchema,
805
+ role: "client"
806
+ });
807
+ const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
808
+ const agents = loadAgents({
809
+ schema: agentConfigSchema,
810
+ agentsDir
811
+ });
812
+ process.stderr.write(`\n Starting client...\n`);
813
+ const runtime = new ClientRuntime(config.server.url);
814
+ for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
815
+ await runtime.start();
816
+ runtime.watchAgentsDir(agentsDir);
817
+ const shutdown = async () => {
818
+ process.stderr.write("\n Shutting down...\n");
819
+ runtime.unwatchAgentsDir();
820
+ await runtime.stop();
821
+ process.exit(0);
822
+ };
823
+ process.on("SIGINT", () => void shutdown());
824
+ process.on("SIGTERM", () => void shutdown());
825
+ await new Promise(() => {});
826
+ } catch (error) {
827
+ if (error.name === "ExitPromptError") {
828
+ process.stderr.write("\n Cancelled.\n");
829
+ return;
830
+ }
831
+ const msg = error instanceof Error ? error.message : String(error);
832
+ process.stderr.write(` Error: ${msg}\n`);
833
+ process.exit(1);
834
+ } finally {
835
+ resetConfig();
836
+ resetConfigMeta();
837
+ }
838
+ });
839
+ }
840
+ //#endregion
456
841
  //#region src/commands/onboard.ts
457
842
  async function promptMissing(args) {
458
843
  if (!args.server) try {
459
- const { resolveServerUrl } = await import("../bootstrap-uyPaaI05.mjs").then((n) => n.n);
844
+ const { resolveServerUrl } = await import("../bootstrap-BnlTKa0H.mjs").then((n) => n.n);
460
845
  resolveServerUrl();
461
846
  } catch {
462
847
  args.server = await input({ message: "Hub server URL:" });
463
848
  saveOnboardState(args);
464
849
  }
465
- const { resolveServerUrl } = await import("../bootstrap-uyPaaI05.mjs").then((n) => n.n);
850
+ const { resolveServerUrl } = await import("../bootstrap-BnlTKa0H.mjs").then((n) => n.n);
466
851
  const serverUrl = resolveServerUrl(args.server).replace(/\/+$/, "");
467
852
  try {
468
853
  const res = await fetch(`${serverUrl}/api/v1/bootstrap/config`);
@@ -474,7 +859,7 @@ async function promptMissing(args) {
474
859
  }
475
860
  let ghUsername = null;
476
861
  try {
477
- const { getGitHubUsername } = await import("../bootstrap-uyPaaI05.mjs").then((n) => n.n);
862
+ const { getGitHubUsername } = await import("../bootstrap-BnlTKa0H.mjs").then((n) => n.n);
478
863
  ghUsername = getGitHubUsername();
479
864
  } catch {}
480
865
  if (!args.id) {
@@ -626,8 +1011,6 @@ function registerServerCommands(program) {
626
1011
  checkDocker(),
627
1012
  checkServerConfig(),
628
1013
  await checkDatabase(),
629
- await checkGitHubToken(),
630
- await checkContextTreeRepo(),
631
1014
  await checkServerHealth()
632
1015
  ]);
633
1016
  });
@@ -660,12 +1043,12 @@ function registerServerCommands(program) {
660
1043
  process.exit(1);
661
1044
  }
662
1045
  });
663
- server.command("admin:create").description("Create an admin user").option("-u, --username <name>", "Admin username", "admin").option("-p, --password <pass>", "Admin password (auto-generated if omitted)").action(async (options) => {
1046
+ server.command("admin:create").description("Create an admin user with organization").option("-u, --username <name>", "Admin username", "admin").option("-n, --name <name>", "Display name", "Admin").option("-o, --org <org>", "Organization slug", "default").option("-p, --password <pass>", "Password (auto-generated if omitted)").action(async (options) => {
664
1047
  try {
665
- const result = await createAdminUser((await initConfig({
1048
+ const result = await createOwner((await initConfig({
666
1049
  schema: serverConfigSchema,
667
1050
  role: "server"
668
- })).database.url, options.username, options.password);
1051
+ })).database.url, options.username, options.org, options.name, options.password);
669
1052
  process.stderr.write(` Admin user "${result.username}" created.\n`);
670
1053
  if (!options.password) process.stderr.write(` Password: ${result.password} (save this — shown only once)\n`);
671
1054
  } catch (error) {
@@ -742,6 +1125,7 @@ registerClientCommands(program);
742
1125
  registerAgentCommands(program);
743
1126
  registerConfigCommands(program);
744
1127
  registerStatusCommand(program);
1128
+ registerConnectCommand(program);
745
1129
  registerOnboardCommand(program);
746
1130
  program.parse();
747
1131
  //#endregion