@agent-team-foundation/first-tree-hub 0.9.3 → 0.9.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.
@@ -1,31 +1,24 @@
1
1
  #!/usr/bin/env node
2
- import "../logger-core-2yeIU1fc-B-__AsQO.mjs";
3
- import { C as serverConfigSchema, _ as loadAgents, b as resetConfig, c as saveCredentials, f as agentConfigSchema, g as initConfig, h as getConfigValue, i as loadCredentials, l as DEFAULT_CONFIG_DIR, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, r as ensureFreshAdminToken, s as saveAgentConfig, u as DEFAULT_DATA_DIR, w as setConfigValue, x as resetConfigMeta, y as readConfigFile } from "../bootstrap-CWcBzk6C.mjs";
4
- import "../observability-CJzDFY_G-CmvgUuzc.mjs";
5
- import { A as printResults, B as SdkError, C as checkDatabase, D as checkServerHealth, E as checkServerConfig, F as stopPostgres, H as cleanWorkspaces, I as ClientRuntime, L as createOwner, O as checkServerReachable, S as checkClientConfig, T as checkNodeVersion, U as applyClientLoggerConfig, V as SessionRegistry, _ as isServiceSupported, a as COMMAND_VERSION, b as runMigrations, c as promptMissingFields, d as onboardCheck, f as onboardCreate, g as installClientService, h as getClientServiceStatus, i as startServer, k as checkWebSocket, l as formatCheckReport, m as runHomeMigration, n as declineUpdate, o as isInteractive, p as saveOnboardState, r as promptUpdate, s as promptAddAgent, t as createExecuteUpdate, u as loadOnboardState, w as checkDocker, x as checkAgentConfigs, y as uninstallClientService, z as FirstTreeHubSDK } from "../core-Y7M3d2aZ.mjs";
2
+ import "../observability-DV_fQKqV-CuLWzBxQ.mjs";
3
+ import { A as checkServerHealth, C as runMigrations, D as checkDocker, E as checkDatabase, G as SdkError, H as setJsonMode, I as stopPostgres, J as applyClientLoggerConfig, K as SessionRegistry, L as ClientRuntime, M as checkWebSocket, N as printResults, O as checkNodeVersion, R as createOwner, S as uninstallClientService, T as checkClientConfig, V as print, W as FirstTreeHubSDK, Y as configureClientLoggerForService, _ as runHomeMigration, a as showServiceLogs, b as isServiceSupported, c as COMMAND_VERSION, d as promptMissingFields, f as formatCheckReport, g as saveOnboardState, h as onboardCreate, i as parseDuration, j as checkServerReachable, k as checkServerConfig, l as isInteractive, m as onboardCheck, n as declineUpdate, o as validateLevel, p as loadOnboardState, q as cleanWorkspaces, r as promptUpdate, s as startServer, t as createExecuteUpdate, u as promptAddAgent, v as getClientServiceStatus, w as checkAgentConfigs, y as installClientService } from "../core-USyOOh7y.mjs";
4
+ import "../logger-core-BTmvdflj-DhdipBkV.mjs";
5
+ import { C as serverConfigSchema, _ as loadAgents, b as resetConfig, c as saveCredentials, d as DEFAULT_HOME_DIR, f as agentConfigSchema, g as initConfig, h as getConfigValue, i as loadCredentials, l as DEFAULT_CONFIG_DIR, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, r as ensureFreshAdminToken, s as saveAgentConfig, u as DEFAULT_DATA_DIR, w as setConfigValue, x as resetConfigMeta, y as readConfigFile } from "../bootstrap-DWifXj9b.mjs";
6
6
  import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-GlaczcVf.mjs";
7
- import { Command } from "commander";
8
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
9
7
  import { join } from "node:path";
8
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
9
+ import { Command } from "commander";
10
10
  import { confirm, input, password, select } from "@inquirer/prompts";
11
11
  //#region src/cli/output.ts
12
- /** Write a success JSON envelope to stdout. */
12
+ /**
13
+ * CLI output re-exports. The underlying implementation lives in
14
+ * `core/output.ts` (the Print layer). Keep these thin wrappers so callers that
15
+ * only depend on `cli/output.ts` keep working during the migration.
16
+ */
13
17
  function success(data) {
14
- process.stdout.write(`${JSON.stringify({
15
- ok: true,
16
- data
17
- })}\n`);
18
+ print.result(data);
18
19
  }
19
- /** Write an error JSON envelope to stderr and exit with the given code. */
20
20
  function fail(code, message, exitCode = 1) {
21
- process.stderr.write(`${JSON.stringify({
22
- ok: false,
23
- error: {
24
- code,
25
- message
26
- }
27
- })}\n`);
28
- process.exit(exitCode);
21
+ return print.fail(code, message, exitCode);
29
22
  }
30
23
  //#endregion
31
24
  //#region src/commands/agent-config.ts
@@ -318,22 +311,22 @@ function registerAgentCommands(program) {
318
311
  mode: 448
319
312
  });
320
313
  setConfigValue(join(agentDir, "agent.yaml"), "agentId", agentId);
321
- process.stderr.write(` Agent "${agentName}" added.\n`);
322
- process.stderr.write(` Config: ${join(agentDir, "agent.yaml")}\n`);
314
+ print.line(` Agent "${agentName}" added.\n`);
315
+ print.line(` Config: ${join(agentDir, "agent.yaml")}\n`);
323
316
  } catch (error) {
324
317
  if (error.name === "ExitPromptError") {
325
- process.stderr.write("\n Cancelled.\n");
318
+ print.line("\n Cancelled.\n");
326
319
  return;
327
320
  }
328
321
  const msg = error instanceof Error ? error.message : String(error);
329
- process.stderr.write(` Error: ${msg}\n`);
322
+ print.line(` Error: ${msg}\n`);
330
323
  process.exit(1);
331
324
  }
332
325
  });
333
326
  agent.command("remove <name>").description("Remove a local agent alias and its runtime data").action((name) => {
334
327
  const agentDir = join(DEFAULT_CONFIG_DIR, "agents", name);
335
328
  if (!existsSync(agentDir)) {
336
- process.stderr.write(` Agent "${name}" not found.\n`);
329
+ print.line(` Agent "${name}" not found.\n`);
337
330
  process.exit(1);
338
331
  }
339
332
  rmSync(agentDir, {
@@ -345,7 +338,7 @@ function registerAgentCommands(program) {
345
338
  force: true
346
339
  });
347
340
  rmSync(join(DEFAULT_DATA_DIR, "sessions", `${name}.json`), { force: true });
348
- process.stderr.write(` Agent "${name}" removed.\n`);
341
+ print.line(` Agent "${name}" removed.\n`);
349
342
  });
350
343
  agent.command("list").description("List locally-configured agents").action(() => {
351
344
  const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
@@ -355,12 +348,12 @@ function registerAgentCommands(program) {
355
348
  agentsDir
356
349
  });
357
350
  if (agents.size === 0) {
358
- process.stderr.write(" No agents configured.\n");
351
+ print.line(" No agents configured.\n");
359
352
  return;
360
353
  }
361
- for (const [name, config] of agents) process.stderr.write(` ${name.padEnd(20)} runtime: ${config.runtime.padEnd(14)} agentId: ${config.agentId}\n`);
354
+ for (const [name, config] of agents) print.line(` ${name.padEnd(20)} runtime: ${config.runtime.padEnd(14)} agentId: ${config.agentId}\n`);
362
355
  } catch {
363
- process.stderr.write(" No agents configured.\n");
356
+ print.line(" No agents configured.\n");
364
357
  }
365
358
  });
366
359
  agent.command("create <name>").description("Create an agent on Hub and bind it locally").requiredOption("--type <type>", "Agent type (human, personal_assistant, autonomous_agent)").requiredOption("--client-id <id>", "Client (machine) that will run this agent — must be owned by you. Run `first-tree-hub client connect` on that machine first.").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) => {
@@ -384,10 +377,10 @@ function registerAgentCommands(program) {
384
377
  });
385
378
  if (!createRes.ok) fail("CREATE_ERROR", (await createRes.json().catch(() => ({}))).error ?? `Failed to create agent (HTTP ${createRes.status})`, 1);
386
379
  const created = await createRes.json();
387
- process.stderr.write(` \u2713 Agent created: ${created.name ?? created.uuid}\n`);
380
+ print.line(` \u2713 Agent created: ${created.name ?? created.uuid}\n`);
388
381
  const agentDir = saveAgentConfig(name, created.uuid, options.runtime);
389
- process.stderr.write(` \u2713 Config saved: ${agentDir}/agent.yaml\n`);
390
- process.stderr.write(" ✓ Agent ready — start the client on that machine to bind\n");
382
+ print.line(` \u2713 Config saved: ${agentDir}/agent.yaml\n`);
383
+ print.line(" ✓ Agent ready — start the client on that machine to bind\n");
391
384
  } catch (error) {
392
385
  fail("CREATE_ERROR", error instanceof Error ? error.message : String(error));
393
386
  }
@@ -413,7 +406,7 @@ function registerAgentCommands(program) {
413
406
  signal: AbortSignal.timeout(1e4)
414
407
  });
415
408
  if (!patchRes.ok) fail("CLAIM_ERROR", (await patchRes.json().catch(() => ({}))).error ?? `Claim failed (HTTP ${patchRes.status})`, 1);
416
- process.stderr.write(` Claimed "${target.name ?? target.uuid}" — now managed by you.\n`);
409
+ print.line(` Claimed "${target.name ?? target.uuid}" — now managed by you.\n`);
417
410
  } catch (error) {
418
411
  fail("CLAIM_ERROR", error instanceof Error ? error.message : String(error));
419
412
  }
@@ -423,7 +416,7 @@ function registerAgentCommands(program) {
423
416
  const ttlMs = Number.parseInt(options?.ttl ?? String(defaultDays), 10) * 24 * 60 * 60 * 1e3;
424
417
  const workspacesDir = join(DEFAULT_DATA_DIR, "workspaces");
425
418
  if (!existsSync(workspacesDir)) {
426
- process.stderr.write(" No workspaces found.\n");
419
+ print.line(" No workspaces found.\n");
427
420
  return;
428
421
  }
429
422
  const agentNames = agentName ? [agentName] : readdirSync(workspacesDir);
@@ -436,9 +429,9 @@ function registerAgentCommands(program) {
436
429
  for (const [chatId, data] of persisted) if (data.status !== "evicted") activeChatIds.add(chatId);
437
430
  const removed = cleanWorkspaces(agentWorkspaceRoot, activeChatIds, ttlMs);
438
431
  totalRemoved += removed.length;
439
- for (const chatId of removed) process.stderr.write(` Removed: ${name}/${chatId}\n`);
432
+ for (const chatId of removed) print.line(` Removed: ${name}/${chatId}\n`);
440
433
  }
441
- process.stderr.write(` ${totalRemoved} workspace(s) cleaned.\n`);
434
+ print.line(` ${totalRemoved} workspace(s) cleaned.\n`);
442
435
  });
443
436
  const bind = agent.command("bind").description("Bind an agent to a client machine or external IM account");
444
437
  bind.command("client <agentName>").description("Bind an unbound agent to a client machine (first-time bind only — ID is immutable once set)").requiredOption("--client-id <id>", "Client (machine) ID — must be owned by you. Run `first-tree-hub client connect` on that machine first.").option("--server <url>", "Hub server URL").action(async (agentName, options) => {
@@ -456,7 +449,7 @@ function registerAgentCommands(program) {
456
449
  signal: AbortSignal.timeout(1e4)
457
450
  });
458
451
  if (!patchRes.ok) fail("BIND_CLIENT_ERROR", (await patchRes.json().catch(() => ({}))).error ?? `Bind failed (HTTP ${patchRes.status})`, 1);
459
- process.stderr.write(` \u2713 Bound "${target.name ?? target.uuid}" to client ${options.clientId}.\n`);
452
+ print.line(` \u2713 Bound "${target.name ?? target.uuid}" to client ${options.clientId}.\n`);
460
453
  success({
461
454
  agentId: target.uuid,
462
455
  clientId: options.clientId
@@ -471,7 +464,7 @@ function registerAgentCommands(program) {
471
464
  const serverUrl = resolveServerUrl(options.server);
472
465
  const { agentId } = resolveLocalAgent(options.agent);
473
466
  await bindFeishuBot(serverUrl, await ensureFreshAccessToken(), agentId, options.appId, options.appSecret);
474
- process.stderr.write("Feishu bot bound successfully.\n");
467
+ print.line("Feishu bot bound successfully.\n");
475
468
  success({
476
469
  platform: "feishu",
477
470
  bound: true
@@ -486,7 +479,7 @@ function registerAgentCommands(program) {
486
479
  const serverUrl = resolveServerUrl(options.server);
487
480
  const { agentId } = resolveLocalAgent(options.agent);
488
481
  await bindFeishuUser(serverUrl, await ensureFreshAccessToken(), agentId, humanAgentId, options.feishuId);
489
- process.stderr.write(`Feishu user ${options.feishuId} bound to ${humanAgentId}.\n`);
482
+ print.line(`Feishu user ${options.feishuId} bound to ${humanAgentId}.\n`);
490
483
  success({
491
484
  platform: "feishu",
492
485
  humanAgentId,
@@ -560,30 +553,30 @@ function registerAgentCommands(program) {
560
553
  if (name) {
561
554
  const ag = data.agents.find((a) => a.agentId === name);
562
555
  if (!ag) {
563
- process.stderr.write(`\n Agent "${name}" is not running.\n\n`);
556
+ print.line(`\n Agent "${name}" is not running.\n\n`);
564
557
  return;
565
558
  }
566
- process.stderr.write(`\n Agent: ${ag.agentId}\n`);
567
- process.stderr.write(` Runtime: ${ag.runtimeType ?? "—"}\n`);
568
- process.stderr.write(` State: ${ag.runtimeState ?? "—"}\n`);
569
- if (ag.activeSessions !== null) process.stderr.write(` Sessions: ${ag.activeSessions} active / ${ag.totalSessions ?? 0} total\n`);
570
- if (ag.clientId) process.stderr.write(` Client: ${ag.clientId}\n`);
571
- process.stderr.write("\n");
559
+ print.line(`\n Agent: ${ag.agentId}\n`);
560
+ print.line(` Runtime: ${ag.runtimeType ?? "—"}\n`);
561
+ print.line(` State: ${ag.runtimeState ?? "—"}\n`);
562
+ if (ag.activeSessions !== null) print.line(` Sessions: ${ag.activeSessions} active / ${ag.totalSessions ?? 0} total\n`);
563
+ if (ag.clientId) print.line(` Client: ${ag.clientId}\n`);
564
+ print.line("\n");
572
565
  return;
573
566
  }
574
- process.stderr.write(`\n Hub: ${serverUrl}\n\n`);
575
- process.stderr.write(` Clients: ${data.clients} connected\n`);
576
- process.stderr.write(` Agents: ${data.running} running / ${data.total} total\n`);
577
- process.stderr.write(` Errors: ${data.byState.error} | Blocked: ${data.byState.blocked} | Working: ${data.byState.working} | Idle: ${data.byState.idle}\n\n`);
567
+ print.line(`\n Hub: ${serverUrl}\n\n`);
568
+ print.line(` Clients: ${data.clients} connected\n`);
569
+ print.line(` Agents: ${data.running} running / ${data.total} total\n`);
570
+ print.line(` Errors: ${data.byState.error} | Blocked: ${data.byState.blocked} | Working: ${data.byState.working} | Idle: ${data.byState.idle}\n\n`);
578
571
  if (data.agents.length > 0) {
579
572
  const header = ` ${"AGENT".padEnd(18)} ${"RUNTIME".padEnd(14)} ${"STATE".padEnd(10)} SESSIONS`;
580
- process.stderr.write(`${header}\n`);
581
- process.stderr.write(` ${"─".repeat(header.length - 2)}\n`);
573
+ print.line(`${header}\n`);
574
+ print.line(` ${"─".repeat(header.length - 2)}\n`);
582
575
  for (const a of data.agents) {
583
576
  const sessions = a.activeSessions !== null ? `${a.activeSessions}/${a.totalSessions ?? 0}` : "—";
584
- process.stderr.write(` ${(a.agentId ?? "").padEnd(18)} ${(a.runtimeType ?? "—").padEnd(14)} ${(a.runtimeState ?? "—").padEnd(10)} ${sessions}\n`);
577
+ print.line(` ${(a.agentId ?? "").padEnd(18)} ${(a.runtimeType ?? "—").padEnd(14)} ${(a.runtimeState ?? "—").padEnd(10)} ${sessions}\n`);
585
578
  }
586
- process.stderr.write("\n");
579
+ print.line("\n");
587
580
  }
588
581
  } catch (error) {
589
582
  fail("STATUS_ERROR", error instanceof Error ? error.message : String(error));
@@ -598,7 +591,7 @@ function registerAgentCommands(program) {
598
591
  signal: AbortSignal.timeout(1e4)
599
592
  });
600
593
  if (!response.ok) fail("RESET_ERROR", `Server returned ${response.status}`, 1);
601
- process.stderr.write(` Agent "${name}" reset to idle.\n`);
594
+ print.line(` Agent "${name}" reset to idle.\n`);
602
595
  } catch (error) {
603
596
  fail("RESET_ERROR", error instanceof Error ? error.message : String(error));
604
597
  }
@@ -616,18 +609,18 @@ function registerAgentCommands(program) {
616
609
  if (!response.ok) fail("FETCH_ERROR", `Server returned ${response.status}`, 1);
617
610
  const sessions = await response.json();
618
611
  if (sessions.length === 0) {
619
- process.stderr.write(`\n No sessions for "${agentName}".\n\n`);
612
+ print.line(`\n No sessions for "${agentName}".\n\n`);
620
613
  return;
621
614
  }
622
- process.stderr.write(`\n Sessions for "${agentName}":\n\n`);
615
+ print.line(`\n Sessions for "${agentName}":\n\n`);
623
616
  const header = ` ${"CHAT".padEnd(40)} ${"STATE".padEnd(12)} ${"RUNTIME".padEnd(10)} LAST ACTIVITY`;
624
- process.stderr.write(`${header}\n`);
625
- process.stderr.write(` ${"─".repeat(header.length - 2)}\n`);
617
+ print.line(`${header}\n`);
618
+ print.line(` ${"─".repeat(header.length - 2)}\n`);
626
619
  for (const s of sessions) {
627
620
  const chatShort = s.chatId.length > 38 ? `${s.chatId.slice(0, 35)}...` : s.chatId;
628
- process.stderr.write(` ${chatShort.padEnd(40)} ${s.state.padEnd(12)} ${(s.runtimeState ?? "—").padEnd(10)} ${s.lastActivityAt}\n`);
621
+ print.line(` ${chatShort.padEnd(40)} ${s.state.padEnd(12)} ${(s.runtimeState ?? "—").padEnd(10)} ${s.lastActivityAt}\n`);
629
622
  }
630
- process.stderr.write("\n");
623
+ print.line("\n");
631
624
  } catch (error) {
632
625
  fail("SESSIONS_ERROR", error instanceof Error ? error.message : String(error));
633
626
  }
@@ -647,7 +640,7 @@ function registerAgentCommands(program) {
647
640
  const body = await response.text();
648
641
  fail("SESSION_CMD_ERROR", `Server returned ${response.status}: ${body}`, 1);
649
642
  }
650
- process.stderr.write(` Session ${cmd}: ${chatId} → sent\n`);
643
+ print.line(` Session ${cmd}: ${chatId} → sent\n`);
651
644
  } catch (error) {
652
645
  fail("SESSION_CMD_ERROR", error instanceof Error ? error.message : String(error));
653
646
  }
@@ -671,9 +664,9 @@ function registerAgentCommands(program) {
671
664
  fail("DM_ERROR", `Failed to create DM: ${dmRes.status} — ${body}`, 1);
672
665
  }
673
666
  const dm = await dmRes.json();
674
- process.stderr.write(`\n Chat with ${targetAgent.displayName ?? targetAgent.name ?? targetAgent.uuid}\n`);
675
- process.stderr.write(` Chat ID: ${dm.id}\n`);
676
- process.stderr.write(` Type a message and press Enter. Ctrl+C to exit.\n\n`);
667
+ print.line(`\n Chat with ${targetAgent.displayName ?? targetAgent.name ?? targetAgent.uuid}\n`);
668
+ print.line(` Chat ID: ${dm.id}\n`);
669
+ print.line(` Type a message and press Enter. Ctrl+C to exit.\n\n`);
677
670
  const rl = (await import("node:readline")).createInterface({
678
671
  input: process.stdin,
679
672
  output: process.stderr,
@@ -694,7 +687,7 @@ function registerAgentCommands(program) {
694
687
  for (const msg of newMessages) {
695
688
  const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
696
689
  const preview = content.length > 500 ? `${content.slice(0, 500)}...` : content;
697
- process.stderr.write(`\r [${targetAgent.displayName ?? targetAgent.name ?? "agent"}] ${preview}\n`);
690
+ print.line(`\r [${targetAgent.displayName ?? targetAgent.name ?? "agent"}] ${preview}\n`);
698
691
  }
699
692
  if (msgData.items.length > 0 && msgData.items[0]) {
700
693
  const newest = msgData.items[0].createdAt;
@@ -725,16 +718,16 @@ function registerAgentCommands(program) {
725
718
  });
726
719
  if (!sendRes.ok) {
727
720
  const body = await sendRes.text();
728
- process.stderr.write(` [error] Failed to send: ${sendRes.status} — ${body}\n`);
721
+ print.line(` [error] Failed to send: ${sendRes.status} — ${body}\n`);
729
722
  } else lastSeenAt = (await sendRes.json()).createdAt;
730
723
  } catch (err) {
731
- process.stderr.write(` [error] ${err instanceof Error ? err.message : String(err)}\n`);
724
+ print.line(` [error] ${err instanceof Error ? err.message : String(err)}\n`);
732
725
  }
733
726
  rl.prompt();
734
727
  });
735
728
  rl.on("close", () => {
736
729
  clearInterval(pollTimer);
737
- process.stderr.write("\n Chat ended.\n");
730
+ print.line("\n Chat ended.\n");
738
731
  process.exit(0);
739
732
  });
740
733
  } catch (error) {
@@ -812,14 +805,14 @@ async function promptReplaceOrCancel(newMemberId, newServerUrl) {
812
805
  const existingOrg = typeof existingPayload?.organizationId === "string" ? existingPayload.organizationId : null;
813
806
  const serviceStatus = getClientServiceStatus();
814
807
  const serviceLine = serviceStatus.state === "active" ? `running (${serviceStatus.detail ?? "live"})` : serviceStatus.state === "inactive" ? `installed but not running${serviceStatus.detail ? ` — ${serviceStatus.detail}` : ""}` : "not installed";
815
- process.stderr.write("\n");
816
- process.stderr.write(" ⚠️ This computer is already connected to the Hub under another account.\n\n");
817
- process.stderr.write(` Existing account: ${existingMember}\n`);
818
- if (existingOrg) process.stderr.write(` Organization: ${existingOrg.slice(0, 8)}\n`);
819
- process.stderr.write(` Server: ${existing.serverUrl}\n`);
820
- process.stderr.write(` Background service: ${serviceLine}\n\n`);
821
- process.stderr.write(" Replacing only affects THIS computer. Your agents, messages, and\n");
822
- process.stderr.write(" settings on the Hub itself are untouched.\n\n");
808
+ print.line("\n");
809
+ print.line(" ⚠️ This computer is already connected to the Hub under another account.\n\n");
810
+ print.line(` Existing account: ${existingMember}\n`);
811
+ if (existingOrg) print.line(` Organization: ${existingOrg.slice(0, 8)}\n`);
812
+ print.line(` Server: ${existing.serverUrl}\n`);
813
+ print.line(` Background service: ${serviceLine}\n\n`);
814
+ print.line(" Replacing only affects THIS computer. Your agents, messages, and\n");
815
+ print.line(" settings on the Hub itself are untouched.\n\n");
823
816
  if (await select({
824
817
  message: "How would you like to continue?",
825
818
  choices: [{
@@ -836,15 +829,15 @@ async function promptReplaceOrCancel(newMemberId, newServerUrl) {
836
829
  return "proceed";
837
830
  }
838
831
  function printIsolationGuide(newServerUrl) {
839
- process.stderr.write("\n Cancelled. The existing account on this computer is untouched.\n\n");
840
- process.stderr.write(" To run this new account alongside it (advanced — no background service):\n\n");
841
- process.stderr.write(" export FIRST_TREE_HUB_HOME=\"$HOME/.first-tree/hub-<label>\"\n");
842
- process.stderr.write(` first-tree-hub client connect ${newServerUrl} --token <token>\n`);
843
- process.stderr.write(" first-tree-hub client start\n\n");
844
- process.stderr.write(" Notes:\n");
845
- process.stderr.write(" - Run the commands in a FRESH terminal (the isolated home must be set first).\n");
846
- process.stderr.write(" - In isolated mode the client stays online only while that terminal runs.\n");
847
- process.stderr.write(" - The main account's background service is not affected.\n\n");
832
+ print.line("\n Cancelled. The existing account on this computer is untouched.\n\n");
833
+ print.line(" To run this new account alongside it (advanced — no background service):\n\n");
834
+ print.line(" export FIRST_TREE_HUB_HOME=\"$HOME/.first-tree/hub-<label>\"\n");
835
+ print.line(` first-tree-hub client connect ${newServerUrl} --token <token>\n`);
836
+ print.line(" first-tree-hub client start\n\n");
837
+ print.line(" Notes:\n");
838
+ print.line(" - Run the commands in a FRESH terminal (the isolated home must be set first).\n");
839
+ print.line(" - In isolated mode the client stays online only while that terminal runs.\n");
840
+ print.line(" - The main account's background service is not affected.\n\n");
848
841
  }
849
842
  /**
850
843
  * Authenticate via connect token — exchange for full JWT credentials.
@@ -863,7 +856,7 @@ async function authenticateWithToken(url, token) {
863
856
  * Authenticate via interactive username/password login.
864
857
  */
865
858
  async function authenticateInteractive(url) {
866
- process.stderr.write("\n Log in to Hub:\n");
859
+ print.line("\n Log in to Hub:\n");
867
860
  const username = await input({ message: " Username:" });
868
861
  const pw = await password({ message: " Password:" });
869
862
  const loginRes = await fetch(`${url}/api/v1/auth/login`, {
@@ -900,30 +893,30 @@ function registerConnectCommand(parent) {
900
893
  }
901
894
  }
902
895
  setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", url);
903
- process.stderr.write(`\n \u2713 Server configured: ${url}\n`);
896
+ print.line(`\n \u2713 Server configured: ${url}\n`);
904
897
  saveCredentials({
905
898
  ...tokens,
906
899
  serverUrl: url
907
900
  });
908
- process.stderr.write(" ✓ Authenticated\n");
901
+ print.line(" ✓ Authenticated\n");
909
902
  resetConfig();
910
903
  resetConfigMeta();
911
904
  const config = await initConfig({
912
905
  schema: clientConfigSchema,
913
906
  role: "client"
914
907
  });
915
- process.stderr.write(` \u2713 Connected as this computer (id: ${config.client.id})\n`);
908
+ print.line(` \u2713 Connected as this computer (id: ${config.client.id})\n`);
916
909
  if (options.service !== false && isServiceSupported()) {
917
910
  const info = installClientService();
918
- process.stderr.write(` \u2713 Installed as a background service (${info.platform}) — you can close this terminal\n\n`);
919
- process.stderr.write(` Unit: ${info.unitPath}\n`);
920
- process.stderr.write(` Logs: ${info.logDir}\n`);
921
- if (info.state === "active" && info.detail) process.stderr.write(` State: running (${info.detail})\n`);
922
- process.stderr.write("\n");
911
+ print.line(` \u2713 Installed as a background service (${info.platform}) — you can close this terminal\n\n`);
912
+ print.line(` Unit: ${info.unitPath}\n`);
913
+ print.line(` Logs: ${info.logDir}\n`);
914
+ if (info.state === "active" && info.detail) print.line(` State: running (${info.detail})\n`);
915
+ print.line("\n");
923
916
  return;
924
917
  }
925
- if (options.service === false) process.stderr.write(" (--no-service) running inline — Ctrl+C to stop\n");
926
- else process.stderr.write(` Background service not supported on ${process.platform}; running inline.\n`);
918
+ if (options.service === false) print.line(" (--no-service) running inline — Ctrl+C to stop\n");
919
+ else print.line(` Background service not supported on ${process.platform}; running inline.\n`);
927
920
  const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
928
921
  const agents = loadAgents({
929
922
  schema: agentConfigSchema,
@@ -941,7 +934,7 @@ function registerConnectCommand(parent) {
941
934
  await runtime.start();
942
935
  runtime.watchAgentsDir(agentsDir);
943
936
  const shutdown = async () => {
944
- process.stderr.write("\n Shutting down...\n");
937
+ print.line("\n Shutting down...\n");
945
938
  runtime.unwatchAgentsDir();
946
939
  await runtime.stop();
947
940
  process.exit(0);
@@ -951,11 +944,11 @@ function registerConnectCommand(parent) {
951
944
  await new Promise(() => {});
952
945
  } catch (error) {
953
946
  if (error.name === "ExitPromptError") {
954
- process.stderr.write("\n Cancelled.\n");
947
+ print.line("\n Cancelled.\n");
955
948
  return;
956
949
  }
957
950
  const msg = error instanceof Error ? error.message : String(error);
958
- process.stderr.write(` Error: ${msg}\n`);
951
+ print.line(` Error: ${msg}\n`);
959
952
  process.exit(1);
960
953
  } finally {
961
954
  resetConfig();
@@ -980,12 +973,13 @@ function registerClientCommands(program) {
980
973
  role: "client"
981
974
  });
982
975
  applyClientLoggerConfig({ level: config.logLevel });
976
+ if (process.env.FIRST_TREE_HUB_SERVICE_MODE === "1") configureClientLoggerForService(join(DEFAULT_HOME_DIR, "logs"));
983
977
  const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
984
978
  const agents = loadAgents({
985
979
  schema: agentConfigSchema,
986
980
  agentsDir
987
981
  });
988
- process.stderr.write(`\n Connecting to ${config.server.url} (client id: ${config.client.id})...\n`);
982
+ print.line(`\n Connecting to ${config.server.url} (client id: ${config.client.id})...\n`);
989
983
  const managed = options.interactive === false;
990
984
  const runtime = new ClientRuntime(config.server.url, config.client.id, {
991
985
  currentVersion: COMMAND_VERSION,
@@ -999,7 +993,7 @@ function registerClientCommands(program) {
999
993
  await runtime.start();
1000
994
  runtime.watchAgentsDir(agentsDir);
1001
995
  const shutdown = async () => {
1002
- process.stderr.write("\n Shutting down...\n");
996
+ print.line("\n Shutting down...\n");
1003
997
  runtime.unwatchAgentsDir();
1004
998
  await runtime.stop();
1005
999
  process.exit(0);
@@ -1009,7 +1003,7 @@ function registerClientCommands(program) {
1009
1003
  await new Promise(() => {});
1010
1004
  } catch (error) {
1011
1005
  const msg = error instanceof Error ? error.message : String(error);
1012
- process.stderr.write(` Error: ${msg}\n`);
1006
+ print.line(` Error: ${msg}\n`);
1013
1007
  process.exit(1);
1014
1008
  } finally {
1015
1009
  resetConfig();
@@ -1017,7 +1011,7 @@ function registerClientCommands(program) {
1017
1011
  }
1018
1012
  });
1019
1013
  client.command("doctor").description("Check client environment readiness").action(async () => {
1020
- process.stderr.write("\n First Tree Hub Client Doctor\n\n");
1014
+ print.line("\n First Tree Hub Client Doctor\n\n");
1021
1015
  printResults([
1022
1016
  checkNodeVersion(),
1023
1017
  checkClientConfig(),
@@ -1027,8 +1021,8 @@ function registerClientCommands(program) {
1027
1021
  ]);
1028
1022
  });
1029
1023
  client.command("stop").description("Stop the client (sends SIGTERM to running process)").action(() => {
1030
- process.stderr.write(" Client stop: use Ctrl+C or `kill` the running process.\n");
1031
- process.stderr.write(" Daemon mode with PID file is planned for a future release.\n");
1024
+ print.line(" Client stop: use Ctrl+C or `kill` the running process.\n");
1025
+ print.line(" Daemon mode with PID file is planned for a future release.\n");
1032
1026
  });
1033
1027
  client.command("status").description("Show client and agent connection status").action(() => {
1034
1028
  const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
@@ -1038,31 +1032,31 @@ function registerClientCommands(program) {
1038
1032
  agentsDir
1039
1033
  });
1040
1034
  if (agents.size === 0) {
1041
- process.stderr.write(" No agents configured.\n");
1035
+ print.line(" No agents configured.\n");
1042
1036
  return;
1043
1037
  }
1044
- process.stderr.write("\n Configured agents:\n\n");
1045
- for (const [name, config] of agents) process.stderr.write(` ${name.padEnd(20)} runtime: ${config.runtime.padEnd(14)} agentId: ${config.agentId}\n`);
1046
- process.stderr.write("\n");
1038
+ print.line("\n Configured agents:\n\n");
1039
+ for (const [name, config] of agents) print.line(` ${name.padEnd(20)} runtime: ${config.runtime.padEnd(14)} agentId: ${config.agentId}\n`);
1040
+ print.line("\n");
1047
1041
  } catch {
1048
- process.stderr.write(" No agents directory found.\n");
1042
+ print.line(" No agents directory found.\n");
1049
1043
  }
1050
1044
  });
1051
1045
  const service = client.command("service").description("Install/uninstall the background service that keeps this computer online");
1052
1046
  service.command("install").description("Install as a background service — auto-starts on login/boot").action(() => {
1053
1047
  if (!isServiceSupported()) {
1054
- process.stderr.write(` Background service is not supported on ${process.platform}.\n Run \`first-tree-hub client start\` manually to keep the computer online.
1048
+ print.line(` Background service is not supported on ${process.platform}.\n Run \`first-tree-hub client start\` manually to keep the computer online.
1055
1049
  `);
1056
1050
  process.exit(1);
1057
1051
  }
1058
1052
  try {
1059
1053
  const info = installClientService();
1060
- process.stderr.write(`\n \u2713 Installed as a background service (${info.platform}).\n`);
1061
- process.stderr.write(` Unit: ${info.unitPath}\n`);
1062
- process.stderr.write(` Logs: ${info.logDir}\n`);
1063
- if (info.state === "active") process.stderr.write(` State: running${info.detail ? ` (${info.detail})` : ""}\n`);
1064
- else process.stderr.write(` State: ${info.state}${info.detail ? ` (${info.detail})` : ""}\n`);
1065
- process.stderr.write("\n You can close this terminal — the computer stays online.\n");
1054
+ print.line(`\n \u2713 Installed as a background service (${info.platform}).\n`);
1055
+ print.line(` Unit: ${info.unitPath}\n`);
1056
+ print.line(` Logs: ${info.logDir}\n`);
1057
+ if (info.state === "active") print.line(` State: running${info.detail ? ` (${info.detail})` : ""}\n`);
1058
+ else print.line(` State: ${info.state}${info.detail ? ` (${info.detail})` : ""}\n`);
1059
+ print.line("\n You can close this terminal — the computer stays online.\n");
1066
1060
  } catch (error) {
1067
1061
  fail("SERVICE_INSTALL_ERROR", error instanceof Error ? error.message : String(error));
1068
1062
  }
@@ -1070,26 +1064,40 @@ function registerClientCommands(program) {
1070
1064
  service.command("status").description("Show background service state").action(() => {
1071
1065
  const info = getClientServiceStatus();
1072
1066
  if (info.platform === "unsupported") {
1073
- process.stderr.write(` Not supported on ${process.platform}.\n`);
1067
+ print.line(` Not supported on ${process.platform}.\n`);
1074
1068
  return;
1075
1069
  }
1076
- process.stderr.write(`\n ${info.platform}: ${info.label}\n`);
1077
- process.stderr.write(` Unit: ${info.unitPath}\n`);
1078
- process.stderr.write(` Logs: ${info.logDir}\n`);
1079
- process.stderr.write(` State: ${info.state}${info.detail ? ` (${info.detail})` : ""}\n\n`);
1070
+ print.line(`\n ${info.platform}: ${info.label}\n`);
1071
+ print.line(` Unit: ${info.unitPath}\n`);
1072
+ print.line(` Logs: ${info.logDir}\n`);
1073
+ print.line(` State: ${info.state}${info.detail ? ` (${info.detail})` : ""}\n\n`);
1080
1074
  });
1081
1075
  service.command("uninstall").description("Stop and remove the background service").action(() => {
1082
1076
  if (!isServiceSupported()) {
1083
- process.stderr.write(` Not supported on ${process.platform}.\n`);
1077
+ print.line(` Not supported on ${process.platform}.\n`);
1084
1078
  return;
1085
1079
  }
1086
1080
  try {
1087
1081
  const info = uninstallClientService();
1088
- process.stderr.write(`\n \u2713 Uninstalled background service (${info.platform}).\n\n`);
1082
+ print.line(`\n \u2713 Uninstalled background service (${info.platform}).\n\n`);
1089
1083
  } catch (error) {
1090
1084
  fail("SERVICE_UNINSTALL_ERROR", error instanceof Error ? error.message : String(error));
1091
1085
  }
1092
1086
  });
1087
+ service.command("logs").description("Read background-service logs (pretty by default)").option("-f, --tail", "follow new lines as they arrive (Ctrl+C to stop)", false).option("--since <duration>", "only show records newer than duration (e.g. 10s, 5m, 2h, 1d)").option("--level <level>", "minimum level (trace|debug|info|warn|error|fatal)").option("--json", "emit raw NDJSON lines instead of pretty formatting", false).action(async (options) => {
1088
+ try {
1089
+ const level = validateLevel(options.level);
1090
+ const sinceMs = options.since ? parseDuration(options.since) : void 0;
1091
+ await showServiceLogs({
1092
+ tail: options.tail === true,
1093
+ level,
1094
+ sinceMs,
1095
+ json: options.json === true
1096
+ });
1097
+ } catch (error) {
1098
+ fail("SERVICE_LOGS_ERROR", error instanceof Error ? error.message : String(error));
1099
+ }
1100
+ });
1093
1101
  client.command("hub-list").description("List clients on the Hub server").option("--server <url>", "Hub server URL").action(async (options) => {
1094
1102
  try {
1095
1103
  const serverUrl = resolveServerUrl(options.server);
@@ -1101,18 +1109,18 @@ function registerClientCommands(program) {
1101
1109
  if (!response.ok) fail("FETCH_ERROR", `Server returned ${response.status}`, 1);
1102
1110
  const clients = await response.json();
1103
1111
  if (clients.length === 0) {
1104
- process.stderr.write(" No clients.\n");
1112
+ print.line(" No clients.\n");
1105
1113
  return;
1106
1114
  }
1107
- process.stderr.write(`\n Clients: ${clients.length}\n\n`);
1115
+ print.line(`\n Clients: ${clients.length}\n\n`);
1108
1116
  const header = ` ${"CLIENT".padEnd(20)} ${"HOST".padEnd(25)} ${"AGENTS".padEnd(8)} CONNECTED`;
1109
- process.stderr.write(`${header}\n`);
1110
- process.stderr.write(` ${"─".repeat(header.length - 2)}\n`);
1117
+ print.line(`${header}\n`);
1118
+ print.line(` ${"─".repeat(header.length - 2)}\n`);
1111
1119
  for (const c of clients) {
1112
1120
  const since = c.connectedAt ? timeSince(c.connectedAt) : "—";
1113
- process.stderr.write(` ${c.id.padEnd(20)} ${(c.hostname ?? "—").padEnd(25)} ${String(c.agentCount).padEnd(8)} ${since}\n`);
1121
+ print.line(` ${c.id.padEnd(20)} ${(c.hostname ?? "—").padEnd(25)} ${String(c.agentCount).padEnd(8)} ${since}\n`);
1114
1122
  }
1115
- process.stderr.write("\n");
1123
+ print.line("\n");
1116
1124
  } catch (error) {
1117
1125
  fail("CLIENT_LIST_ERROR", error instanceof Error ? error.message : String(error));
1118
1126
  }
@@ -1127,7 +1135,7 @@ function registerClientCommands(program) {
1127
1135
  signal: AbortSignal.timeout(1e4)
1128
1136
  });
1129
1137
  if (!response.ok) fail("DISCONNECT_ERROR", `Server returned ${response.status}`, 1);
1130
- process.stderr.write(` Client "${clientId}" disconnected.\n`);
1138
+ print.line(` Client "${clientId}" disconnected.\n`);
1131
1139
  } catch (error) {
1132
1140
  fail("DISCONNECT_ERROR", error instanceof Error ? error.message : String(error));
1133
1141
  }
@@ -1170,10 +1178,10 @@ function registerConfigCommands(program) {
1170
1178
  schema: flags.client ? clientConfigSchema : serverConfigSchema,
1171
1179
  role: flags.client ? "client" : "server"
1172
1180
  });
1173
- process.stderr.write("\n Configuration saved.\n");
1181
+ print.line("\n Configuration saved.\n");
1174
1182
  } catch (error) {
1175
1183
  if (error.name === "ExitPromptError") {
1176
- process.stderr.write("\n Cancelled.\n");
1184
+ print.line("\n Cancelled.\n");
1177
1185
  return;
1178
1186
  }
1179
1187
  throw error;
@@ -1186,27 +1194,27 @@ function registerConfigCommands(program) {
1186
1194
  else if (value === "false") parsed = false;
1187
1195
  else if (/^\d+$/.test(value)) parsed = Number(value);
1188
1196
  setConfigValue(path, key, parsed);
1189
- process.stderr.write(` Set ${key} in ${path}\n`);
1197
+ print.line(` Set ${key} in ${path}\n`);
1190
1198
  });
1191
1199
  addScopeOptions(config.command("get").description("Get a config value")).argument("<key>", "Config key (dot notation)").option("--show-secrets", "Show secret values in plaintext").action((key, flags) => {
1192
1200
  const { path, schema } = resolveConfigPath(flags);
1193
1201
  const value = getConfigValue(path, key);
1194
- if (value === void 0) process.stderr.write(` ${key}: (not set)\n`);
1202
+ if (value === void 0) print.line(` ${key}: (not set)\n`);
1195
1203
  else {
1196
1204
  const display = isSecretField(schema, key) && !flags.showSecrets ? "***" : String(value);
1197
- process.stderr.write(` ${key}: ${display}\n`);
1205
+ print.line(` ${key}: ${display}\n`);
1198
1206
  }
1199
1207
  });
1200
1208
  addScopeOptions(config.command("list").description("List all config values")).option("--show-secrets", "Show secret values in plaintext").action((flags) => {
1201
1209
  const { path, schema } = resolveConfigPath(flags);
1202
1210
  const values = readConfigFile(path);
1203
1211
  if (Object.keys(values).length === 0) {
1204
- process.stderr.write(` No config found at ${path}\n`);
1212
+ print.line(` No config found at ${path}\n`);
1205
1213
  return;
1206
1214
  }
1207
- process.stderr.write(`\n Config: ${path}\n\n`);
1215
+ print.line(`\n Config: ${path}\n\n`);
1208
1216
  printFlat(values, schema, "", flags.showSecrets ?? false);
1209
- process.stderr.write("\n");
1217
+ print.line("\n");
1210
1218
  });
1211
1219
  }
1212
1220
  function printFlat(obj, schema, prefix, showSecrets) {
@@ -1215,7 +1223,7 @@ function printFlat(obj, schema, prefix, showSecrets) {
1215
1223
  if (typeof value === "object" && value !== null && !Array.isArray(value)) printFlat(value, schema, fullKey, showSecrets);
1216
1224
  else {
1217
1225
  const display = isSecretField(schema, fullKey) && !showSecrets ? "***" : String(value);
1218
- process.stderr.write(` ${fullKey.padEnd(30)} ${display}\n`);
1226
+ print.line(` ${fullKey.padEnd(30)} ${display}\n`);
1219
1227
  }
1220
1228
  }
1221
1229
  }
@@ -1240,13 +1248,13 @@ function isSecretField(schema, dotPath) {
1240
1248
  //#region src/commands/onboard.ts
1241
1249
  async function promptMissing(args) {
1242
1250
  if (!args.server) try {
1243
- const { resolveServerUrl } = await import("../bootstrap-CWcBzk6C.mjs").then((n) => n.t);
1251
+ const { resolveServerUrl } = await import("../bootstrap-DWifXj9b.mjs").then((n) => n.t);
1244
1252
  resolveServerUrl();
1245
1253
  } catch {
1246
1254
  args.server = await input({ message: "Hub server URL:" });
1247
1255
  saveOnboardState(args);
1248
1256
  }
1249
- const { loadCredentials } = await import("../bootstrap-CWcBzk6C.mjs").then((n) => n.t);
1257
+ const { loadCredentials } = await import("../bootstrap-DWifXj9b.mjs").then((n) => n.t);
1250
1258
  if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub client connect <server-url>` before onboarding.");
1251
1259
  if (!args.id) {
1252
1260
  args.id = await input({ message: "Agent ID:" });
@@ -1345,7 +1353,7 @@ function registerOnboardCommand(program) {
1345
1353
  if (args.check) {
1346
1354
  const items = await onboardCheck(args);
1347
1355
  const report = formatCheckReport(items);
1348
- process.stderr.write(`\nOnboard Check: ${args.id ?? "(no id)"}\n\n${report}\n\n`);
1356
+ print.line(`\nOnboard Check: ${args.id ?? "(no id)"}\n\n${report}\n\n`);
1349
1357
  if (items.some((i) => i.status === "missing_required" || i.status === "error")) process.exit(1);
1350
1358
  return;
1351
1359
  }
@@ -1353,14 +1361,14 @@ function registerOnboardCommand(program) {
1353
1361
  const items = await onboardCheck(args);
1354
1362
  if (items.some((i) => i.status === "missing_required" || i.status === "error")) {
1355
1363
  const report = formatCheckReport(items);
1356
- process.stderr.write(`\nOnboard Check: ${args.id ?? "(no id)"}\n\n${report}\n\n`);
1364
+ print.line(`\nOnboard Check: ${args.id ?? "(no id)"}\n\n${report}\n\n`);
1357
1365
  fail("MISSING_PARAMS", "Required parameters are missing. See checklist above.");
1358
1366
  }
1359
1367
  await onboardCreate(args);
1360
1368
  } catch (error) {
1361
1369
  const msg = error instanceof Error ? error.message : String(error);
1362
1370
  if (isInteractive()) {
1363
- process.stderr.write(`\n\u274C ${msg}\n\n`);
1371
+ print.line(`\n\u274C ${msg}\n\n`);
1364
1372
  process.exit(1);
1365
1373
  }
1366
1374
  fail("ONBOARD_ERROR", msg);
@@ -1379,22 +1387,22 @@ function registerServerCommands(program) {
1379
1387
  });
1380
1388
  } catch (error) {
1381
1389
  const msg = error instanceof Error ? error.message : String(error);
1382
- process.stderr.write(`\n Error: ${msg}\n\n`);
1390
+ print.line(`\n Error: ${msg}\n\n`);
1383
1391
  process.exit(1);
1384
1392
  }
1385
1393
  });
1386
1394
  server.command("stop").description("Stop the managed PostgreSQL container").action(() => {
1387
1395
  try {
1388
- if (stopPostgres()) process.stderr.write(" PostgreSQL container stopped.\n");
1389
- else process.stderr.write(" No managed PostgreSQL container found.\n");
1396
+ if (stopPostgres()) print.line(" PostgreSQL container stopped.\n");
1397
+ else print.line(" No managed PostgreSQL container found.\n");
1390
1398
  } catch (error) {
1391
1399
  const msg = error instanceof Error ? error.message : String(error);
1392
- process.stderr.write(` Error stopping PostgreSQL: ${msg}\n`);
1400
+ print.line(` Error stopping PostgreSQL: ${msg}\n`);
1393
1401
  process.exit(1);
1394
1402
  }
1395
1403
  });
1396
1404
  server.command("doctor").description("Check server environment readiness").action(async () => {
1397
- process.stderr.write("\n First Tree Hub Server Doctor\n\n");
1405
+ print.line("\n First Tree Hub Server Doctor\n\n");
1398
1406
  printResults([
1399
1407
  checkNodeVersion(),
1400
1408
  checkDocker(),
@@ -1409,13 +1417,13 @@ function registerServerCommands(program) {
1409
1417
  const res = await fetch(`${url}/api/v1/health`);
1410
1418
  if (res.ok) {
1411
1419
  const data = await res.json();
1412
- console.log(JSON.stringify(data, null, 2));
1420
+ process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
1413
1421
  } else {
1414
- process.stderr.write(` Server returned ${res.status}\n`);
1422
+ print.line(` Server returned ${res.status}\n`);
1415
1423
  process.exit(1);
1416
1424
  }
1417
1425
  } catch {
1418
- process.stderr.write(` Cannot connect to ${url}\n`);
1426
+ print.line(` Cannot connect to ${url}\n`);
1419
1427
  process.exit(1);
1420
1428
  }
1421
1429
  });
@@ -1425,10 +1433,10 @@ function registerServerCommands(program) {
1425
1433
  schema: serverConfigSchema,
1426
1434
  role: "server"
1427
1435
  })).database.url);
1428
- process.stderr.write(` Migrations complete (${tableCount} tables)\n`);
1436
+ print.line(` Migrations complete (${tableCount} tables)\n`);
1429
1437
  } catch (error) {
1430
1438
  const msg = error instanceof Error ? error.message : String(error);
1431
- process.stderr.write(` Error: ${msg}\n`);
1439
+ print.line(` Error: ${msg}\n`);
1432
1440
  process.exit(1);
1433
1441
  }
1434
1442
  });
@@ -1438,11 +1446,11 @@ function registerServerCommands(program) {
1438
1446
  schema: serverConfigSchema,
1439
1447
  role: "server"
1440
1448
  })).database.url, options.username, options.org, options.name, options.password);
1441
- process.stderr.write(` Admin user "${result.username}" created.\n`);
1442
- if (!options.password) process.stderr.write(` Password: ${result.password} (save this — shown only once)\n`);
1449
+ print.line(` Admin user "${result.username}" created.\n`);
1450
+ if (!options.password) print.line(` Password: ${result.password} (save this — shown only once)\n`);
1443
1451
  } catch (error) {
1444
1452
  const msg = error instanceof Error ? error.message : String(error);
1445
- process.stderr.write(` Error: ${msg}\n`);
1453
+ print.line(` Error: ${msg}\n`);
1446
1454
  process.exit(1);
1447
1455
  }
1448
1456
  });
@@ -1451,7 +1459,7 @@ function registerServerCommands(program) {
1451
1459
  //#region src/commands/status.ts
1452
1460
  function registerStatusCommand(program) {
1453
1461
  program.command("status").description("Global overview — server health + configured agents").action(async () => {
1454
- process.stderr.write("\n");
1462
+ print.line("\n");
1455
1463
  const serverConfig = readConfigFile(join(DEFAULT_CONFIG_DIR, "server.yaml"));
1456
1464
  const serverPort = getNestedValue(serverConfig, "server.port") ?? 8e3;
1457
1465
  const serverUrl = `http://${getNestedValue(serverConfig, "server.host") ?? "127.0.0.1"}:${serverPort}`;
@@ -1460,31 +1468,31 @@ function registerStatusCommand(program) {
1460
1468
  if (res.ok) {
1461
1469
  const data = await res.json();
1462
1470
  const uptime = data.uptime_seconds ? formatUptime(data.uptime_seconds) : "unknown";
1463
- process.stderr.write(` Server: ✓ running (${serverUrl}, uptime: ${uptime})\n`);
1464
- } else process.stderr.write(` Server: ✗ unhealthy (${res.status})\n`);
1471
+ print.line(` Server: ✓ running (${serverUrl}, uptime: ${uptime})\n`);
1472
+ } else print.line(` Server: ✗ unhealthy (${res.status})\n`);
1465
1473
  } catch {
1466
- process.stderr.write(` Server: ✗ not running (${serverUrl})\n`);
1474
+ print.line(` Server: ✗ not running (${serverUrl})\n`);
1467
1475
  }
1468
1476
  const dbProvider = getNestedValue(serverConfig, "database.provider") ?? "unknown";
1469
1477
  const hasDbUrl = getNestedValue(serverConfig, "database.url") !== void 0;
1470
- process.stderr.write(` Database: ${hasDbUrl ? "✓ configured" : "✗ not configured"} (${dbProvider})\n`);
1478
+ print.line(` Database: ${hasDbUrl ? "✓ configured" : "✗ not configured"} (${dbProvider})\n`);
1471
1479
  const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
1472
1480
  if (existsSync(agentsDir)) try {
1473
1481
  const agents = loadAgents({
1474
1482
  schema: agentConfigSchema,
1475
1483
  agentsDir
1476
1484
  });
1477
- process.stderr.write(` Agents: ${agents.size} configured\n`);
1485
+ print.line(` Agents: ${agents.size} configured\n`);
1478
1486
  } catch {
1479
- process.stderr.write(" Agents: error reading config\n");
1487
+ print.line(" Agents: error reading config\n");
1480
1488
  }
1481
- else process.stderr.write(" Agents: 0 configured\n");
1489
+ else print.line(" Agents: 0 configured\n");
1482
1490
  const clientConfigPath = join(DEFAULT_CONFIG_DIR, "client.yaml");
1483
1491
  if (existsSync(clientConfigPath)) {
1484
1492
  const clientServerUrl = getNestedValue(readConfigFile(clientConfigPath), "server.url");
1485
- process.stderr.write(` Client: configured → ${clientServerUrl}\n`);
1486
- } else process.stderr.write(" Client: not configured\n");
1487
- process.stderr.write("\n");
1493
+ print.line(` Client: configured → ${clientServerUrl}\n`);
1494
+ } else print.line(" Client: not configured\n");
1495
+ print.line("\n");
1488
1496
  });
1489
1497
  }
1490
1498
  function getNestedValue(obj, dotPath) {
@@ -1508,7 +1516,21 @@ function formatUptime(seconds) {
1508
1516
  //#region src/cli/index.ts
1509
1517
  runHomeMigration();
1510
1518
  const program = new Command();
1511
- program.name("first-tree-hub").description("First Tree Hub — centralized collaboration platform for agent teams").version(COMMAND_VERSION);
1519
+ program.name("first-tree-hub").description("First Tree Hub — centralized collaboration platform for agent teams").version(COMMAND_VERSION).option("--json", "emit only machine-readable JSON on stdout; silence human status lines on stderr").option("--verbose", "raise log level to debug (overrides FIRST_TREE_HUB_LOG_LEVEL)").hook("preAction", (thisCommand) => {
1520
+ const opts = thisCommand.optsWithGlobals();
1521
+ const json = opts.json === true || process.env.FIRST_TREE_HUB_JSON === "1";
1522
+ setJsonMode(json);
1523
+ if (opts.verbose) applyClientLoggerConfig({
1524
+ level: "debug",
1525
+ explicit: true
1526
+ });
1527
+ else if (process.env.FIRST_TREE_HUB_LOG_LEVEL) applyClientLoggerConfig({ explicit: true });
1528
+ else if (json) applyClientLoggerConfig({
1529
+ level: "error",
1530
+ explicit: true
1531
+ });
1532
+ else applyClientLoggerConfig({ level: "warn" });
1533
+ });
1512
1534
  registerServerCommands(program);
1513
1535
  registerClientCommands(program);
1514
1536
  registerAgentCommands(program);