@agent-team-foundation/first-tree-hub 0.12.9 → 0.13.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,20 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import "../observability-BAScT_5S-BcW9HgkG.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 fail, b as onboardCreate, c as detectInstallMode, ct as ClientUserMismatchError, d as startServer, dt as SessionRegistry, et as removeLocalAgent, f as reconcileLocalRuntimeProviders, ft as cleanWorkspaces, g as promptMissingFields, h as promptAddAgent, ht as configureClientLoggerForService, i as createExecuteUpdate, it as resolveSenderName, j as checkNodeVersion, k as checkDatabase, l as fetchLatestVersion, lt as FirstTreeHubSDK, m as isInteractive, mt as applyClientLoggerConfig, o as promptUpdate, ot as success, p as uploadClientCapabilities, pt as probeCapabilities, r as registerSaaSConnectCommand, rt as resolveReplyToFromEnv, s as PACKAGE_NAME, st as ClientOrgMismatchError, tt as createOwner, u as installGlobalLatest, ut as SdkError, v as loadOnboardState, w as migrateLocalAgentDirs, x as saveOnboardState, y as onboardCheck, z as installClientService } from "../saas-connect-CXZhK485.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 fail, b as onboardCreate, c as detectInstallMode, ct as ClientUserMismatchError, d as startServer, dt as SessionRegistry, et as removeLocalAgent, f as reconcileLocalRuntimeProviders, ft as cleanWorkspaces, g as promptMissingFields, h as promptAddAgent, ht as configureClientLoggerForService, i as createExecuteUpdate, it as resolveSenderName, j as checkNodeVersion, k as checkDatabase, l as fetchLatestVersion, lt as FirstTreeHubSDK, m as isInteractive, mt as applyClientLoggerConfig, o as promptUpdate, ot as success, p as uploadClientCapabilities, pt as probeCapabilities, r as registerSaaSConnectCommand, rt as resolveReplyToFromEnv, s as PACKAGE_NAME, st as ClientOrgMismatchError, tt as createOwner, u as installGlobalLatest, ut as SdkError, v as loadOnboardState, w as migrateLocalAgentDirs, x as saveOnboardState, y as onboardCheck, z as installClientService } from "../saas-connect-Bb5LR4y6.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-BCZC1ki6.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, v as initConfig, w as resolveConfigReadonly, x as readConfigFile, y as loadAgents } from "../bootstrap-Cya2OoHz.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-CnjqakXS.mjs";
8
- import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-DrnBbl8T.mjs";
9
- import "../errors-CF5evtJt-B0NTIVPt.mjs";
7
+ import "../dist-C8yStx2L.mjs";
8
+ import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-D_vnqC6a.mjs";
9
+ import "../errors-LPcARA4K-Dbrptiyz.mjs";
10
10
  import "../src-DNBS5Yjj.mjs";
11
- import "../client-OMwJMCQt-R1T06ZH6.mjs";
12
- import "../invitation-Bg0TRiyx-BsZH4GCS.mjs";
11
+ import "../client-BH4CmUL0-CybE3kuP.mjs";
12
+ import "../invitation-DZO4NX3P-BPxTeHf-.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";
16
16
  import { Command } from "commander";
17
- import { confirm, input, password, select } from "@inquirer/prompts";
17
+ import { confirm, input, select } from "@inquirer/prompts";
18
18
  //#region src/commands/agent-config.ts
19
19
  async function resolveAgentRecord(serverUrl, adminToken, agentName) {
20
20
  const res = await cliFetch(`${serverUrl}/api/v1/me/managed-agents`, {
@@ -78,7 +78,7 @@ function printConfig(cfg) {
78
78
  }
79
79
  function registerAgentConfigCommands(parent) {
80
80
  const config = parent.command("config").description("Manage agent runtime configuration (Step 8)");
81
- config.command("get <agent>").description("Print the current runtime config for an agent").action(async (agentName) => {
81
+ config.command("show <agent>").description("Show the current runtime config for an agent").action(async (agentName) => {
82
82
  const serverUrl = resolveServerUrl(process.env.FIRST_TREE_HUB_SERVER_URL);
83
83
  const adminToken = await ensureFreshAdminToken();
84
84
  const { uuid } = await resolveAgentRecord(serverUrl, adminToken, agentName);
@@ -209,7 +209,7 @@ const DEFAULT_WORKSPACE_TTL_MS = 10080 * 60 * 1e3;
209
209
  * agents configured they must pick one with `--agent <name>` (next step of
210
210
  * CLI polish) or rely on a single entry.
211
211
  */
212
- function resolveLocalAgent(agentName) {
212
+ function resolveLocalAgent$1(agentName) {
213
213
  const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
214
214
  const agents = loadAgents({
215
215
  schema: agentConfigSchema,
@@ -223,8 +223,8 @@ function resolveLocalAgent(agentName) {
223
223
  let resolvedName;
224
224
  if (resolution.kind === "ok") resolvedName = resolution.name;
225
225
  else if (resolution.kind === "none") fail("MISSING_AGENT", "No agent configured. Run `first-tree-hub agent add` first.", 2);
226
- else if (resolution.kind === "envMismatch") fail("ENV_AGENT_NOT_LOCAL", `FIRST_TREE_HUB_AGENT_ID="${resolution.envAgentId}" is not configured on this machine. Available local agents: ${resolution.available.join(", ")}. Pick one explicitly: \`first-tree-hub agent send --agent <senderName> <recipientName> "..."\`.`, 2);
227
- else fail("AMBIGUOUS_AGENT", `Multiple agents are configured on this machine (${resolution.available.join(", ")}) and FIRST_TREE_HUB_AGENT_ID is not set, so the CLI can't tell which one is the sender. Specify it explicitly: \`first-tree-hub agent send --agent <senderName> <recipientName> "..."\` (--agent picks the SENDER; the recipient is the next positional argument).`, 2);
226
+ else if (resolution.kind === "envMismatch") fail("ENV_AGENT_NOT_LOCAL", `FIRST_TREE_HUB_AGENT_ID="${resolution.envAgentId}" is not configured on this machine. Available local agents: ${resolution.available.join(", ")}. Pick one explicitly with \`--agent <senderName>\`.`, 2);
227
+ else fail("AMBIGUOUS_AGENT", `Multiple agents are configured on this machine (${resolution.available.join(", ")}) and FIRST_TREE_HUB_AGENT_ID is not set, so the CLI can't tell which one is the sender. Specify it explicitly with \`--agent <senderName>\`.`, 2);
228
228
  const cfg = agents.get(resolvedName);
229
229
  if (!cfg) fail("UNKNOWN_AGENT", `Agent "${resolvedName}" not found in ${agentsDir}`, 2);
230
230
  let serverUrl;
@@ -238,8 +238,8 @@ function resolveLocalAgent(agentName) {
238
238
  agentId: cfg.agentId
239
239
  };
240
240
  }
241
- function createSdk(agentName) {
242
- const { serverUrl, agentId } = resolveLocalAgent(agentName);
241
+ function createSdk$1(agentName) {
242
+ const { serverUrl, agentId } = resolveLocalAgent$1(agentName);
243
243
  return new FirstTreeHubSDK({
244
244
  serverUrl,
245
245
  getAccessToken: (opts) => ensureFreshAccessToken(opts),
@@ -247,7 +247,7 @@ function createSdk(agentName) {
247
247
  userAgent: CLI_USER_AGENT
248
248
  });
249
249
  }
250
- function handleSdkError(error) {
250
+ function handleSdkError$1(error) {
251
251
  if (error instanceof SdkError) {
252
252
  const exitCode = error.statusCode === 401 ? 3 : 1;
253
253
  fail(`HTTP_${error.statusCode}`, error.message, exitCode);
@@ -255,31 +255,12 @@ function handleSdkError(error) {
255
255
  if (error instanceof TypeError && "cause" in error) fail("CONNECTION_ERROR", `Cannot connect to server: ${error.message}`, 6);
256
256
  fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), 1);
257
257
  }
258
- function parseLimit(value, max) {
258
+ function parseLimit$1(value, max) {
259
259
  const limit = Number.parseInt(value, 10);
260
260
  if (Number.isNaN(limit) || limit < 1 || limit > max) fail("INVALID_LIMIT", `Limit must be between 1 and ${max}.`, 2);
261
261
  return limit;
262
262
  }
263
- const MAX_STDIN_BYTES = 10 * 1024 * 1024;
264
- function readStdin() {
265
- if (process.stdin.isTTY) return Promise.resolve(null);
266
- return new Promise((resolve, reject) => {
267
- const chunks = [];
268
- let totalSize = 0;
269
- process.stdin.on("data", (chunk) => {
270
- totalSize += chunk.length;
271
- if (totalSize > MAX_STDIN_BYTES) {
272
- process.stdin.destroy();
273
- reject(/* @__PURE__ */ new Error(`stdin exceeds ${MAX_STDIN_BYTES} bytes`));
274
- return;
275
- }
276
- chunks.push(chunk);
277
- });
278
- process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
279
- process.stdin.on("error", reject);
280
- });
281
- }
282
- async function resolveAgent(serverUrl, adminToken, agentName) {
263
+ async function resolveAgent$1(serverUrl, adminToken, agentName) {
283
264
  const res = await cliFetch(`${serverUrl}/api/v1/me/managed-agents`, {
284
265
  headers: { Authorization: `Bearer ${adminToken}` },
285
266
  signal: AbortSignal.timeout(1e4)
@@ -293,7 +274,7 @@ async function resolveAgent(serverUrl, adminToken, agentName) {
293
274
  * Read the persisted `client.id` from `client.yaml`. Required by `agent
294
275
  * prune` to filter the user-scoped `listMyAgents` response down to "what
295
276
  * actually binds on THIS machine". `fail()` instead of throwing so the
296
- * "no client.yaml — run client connect first" path renders as a clean
277
+ * "no client.yaml — run connect <token> first" path renders as a clean
297
278
  * CLI error rather than a stack trace.
298
279
  */
299
280
  function readClientId() {
@@ -430,7 +411,7 @@ function registerAgentCommands(program) {
430
411
  fail("LIST_ERROR", error instanceof Error ? error.message : String(error));
431
412
  }
432
413
  });
433
- 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("--org <id>", "Target organization id (required when you belong to multiple orgs)").option("--server <url>", "Hub server URL").action(async (name, options) => {
414
+ 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 connect <token>` on that machine first.").option("--runtime <runtime>", "Runtime handler (default: claude-code)", "claude-code").option("--display-name <name>", "Display name").option("--org <id>", "Target organization id (required when you belong to multiple orgs)").option("--server <url>", "Hub server URL").action(async (name, options) => {
434
415
  try {
435
416
  const serverUrl = resolveServerUrl(options.server);
436
417
  const adminToken = await ensureFreshAccessToken();
@@ -487,7 +468,7 @@ function registerAgentCommands(program) {
487
468
  });
488
469
  if (!meRes.ok) fail("ME_ERROR", `Failed to fetch current member (HTTP ${meRes.status})`, 1);
489
470
  const me = await meRes.json();
490
- const target = await resolveAgent(serverUrl, accessToken, agentName);
471
+ const target = await resolveAgent$1(serverUrl, accessToken, agentName);
491
472
  const patchRes = await cliFetch(`${serverUrl}/api/v1/agents/${target.uuid}`, {
492
473
  method: "PATCH",
493
474
  headers: {
@@ -526,11 +507,11 @@ function registerAgentCommands(program) {
526
507
  print.line(` ${totalRemoved} workspace(s) cleaned.\n`);
527
508
  });
528
509
  const bind = agent.command("bind").description("Bind an agent to a client machine or external IM account");
529
- 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) => {
510
+ 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 connect <token>` on that machine first.").option("--server <url>", "Hub server URL").action(async (agentName, options) => {
530
511
  try {
531
512
  const serverUrl = resolveServerUrl(options.server);
532
513
  const accessToken = await ensureFreshAccessToken();
533
- const target = await resolveAgent(serverUrl, accessToken, agentName);
514
+ const target = await resolveAgent$1(serverUrl, accessToken, agentName);
534
515
  const patchRes = await cliFetch(`${serverUrl}/api/v1/agents/${target.uuid}`, {
535
516
  method: "PATCH",
536
517
  headers: {
@@ -554,7 +535,7 @@ function registerAgentCommands(program) {
554
535
  try {
555
536
  if (options.platform !== "feishu") fail("UNSUPPORTED_PLATFORM", `Platform "${options.platform}" is not supported. Use "feishu".`);
556
537
  const serverUrl = resolveServerUrl(options.server);
557
- const { agentId } = resolveLocalAgent(options.agent);
538
+ const { agentId } = resolveLocalAgent$1(options.agent);
558
539
  await bindFeishuBot(serverUrl, await ensureFreshAccessToken(), agentId, options.appId, options.appSecret);
559
540
  print.line("Feishu bot bound successfully.\n");
560
541
  success({
@@ -569,7 +550,7 @@ function registerAgentCommands(program) {
569
550
  try {
570
551
  if (options.platform !== "feishu") fail("UNSUPPORTED_PLATFORM", `Platform "${options.platform}" is not supported. Use "feishu".`);
571
552
  const serverUrl = resolveServerUrl(options.server);
572
- const { agentId } = resolveLocalAgent(options.agent);
553
+ const { agentId } = resolveLocalAgent$1(options.agent);
573
554
  await bindFeishuUser(serverUrl, await ensureFreshAccessToken(), agentId, humanAgentId, options.feishuId);
574
555
  print.line(`Feishu user ${options.feishuId} bound to ${humanAgentId}.\n`);
575
556
  success({
@@ -581,62 +562,6 @@ function registerAgentCommands(program) {
581
562
  fail("BIND_USER_ERROR", error instanceof Error ? error.message : String(error));
582
563
  }
583
564
  });
584
- agent.command("send <target> [message]").description("Send a message to an agent or chat").option("-f, --format <format>", "Message format (text|markdown|card)", "text").option("--chat", "Treat target as chat ID instead of agent ID").option("-m, --metadata <json>", "JSON metadata to attach").option("--reply-to <messageId>", "Message ID to reply to").option("--reply-to-inbox <inboxId>", "Cross-chat reply target inbox").option("--reply-to-chat <chatId>", "Cross-chat reply target chat").option("--agent <name>", "Agent name on the Hub (default: first configured on this client)").action(async (target, message, options) => {
585
- try {
586
- const content = message ?? await readStdin();
587
- if (!content) fail("NO_MESSAGE", "No message provided. Pass as argument or pipe via stdin.", 2);
588
- let metadata;
589
- if (options.metadata) try {
590
- metadata = JSON.parse(options.metadata);
591
- } catch {
592
- fail("INVALID_METADATA", "Metadata must be valid JSON.", 2);
593
- }
594
- const { replyToInbox, replyToChat } = resolveReplyToFromEnv(process.env, {
595
- replyToInbox: options.replyToInbox,
596
- replyToChat: options.replyToChat
597
- });
598
- const sdk = createSdk(options.agent);
599
- if (options.chat) success(await sdk.sendMessage(target, {
600
- format: options.format,
601
- content,
602
- metadata,
603
- inReplyTo: options.replyTo,
604
- replyToInbox,
605
- replyToChat
606
- }));
607
- else success(await sdk.sendToAgent(target, {
608
- format: options.format,
609
- content,
610
- metadata,
611
- replyToInbox,
612
- replyToChat
613
- }));
614
- } catch (error) {
615
- handleSdkError(error);
616
- }
617
- });
618
- agent.command("chats").description("List chats this agent participates in").option("-l, --limit <number>", "Maximum chats to return (1-100)", "20").option("--cursor <cursor>", "Pagination cursor from previous response").option("--agent <name>", "Agent name on the Hub (default: first configured on this client)").action(async (options) => {
619
- try {
620
- const limit = parseLimit(options.limit, 100);
621
- success(await createSdk(options.agent).listChats({
622
- limit,
623
- cursor: options.cursor
624
- }));
625
- } catch (error) {
626
- handleSdkError(error);
627
- }
628
- });
629
- agent.command("history <chatId>").description("View message history in a chat").option("-l, --limit <number>", "Maximum messages to return (1-100)", "20").option("--cursor <cursor>", "Pagination cursor from previous response").option("--agent <name>", "Agent name on the Hub (default: first configured on this client)").action(async (chatId, options) => {
630
- try {
631
- const limit = parseLimit(options.limit, 100);
632
- success(await createSdk(options.agent).listMessages(chatId, {
633
- limit,
634
- cursor: options.cursor
635
- }));
636
- } catch (error) {
637
- handleSdkError(error);
638
- }
639
- });
640
565
  agent.command("status [name]").description("Show agent runtime status from Hub server").option("--server <url>", "Hub server URL").action(async (name, options) => {
641
566
  try {
642
567
  const serverUrl = resolveServerUrl(options?.server);
@@ -720,11 +645,12 @@ function registerAgentCommands(program) {
720
645
  fail("RESET_ERROR", error instanceof Error ? error.message : String(error));
721
646
  }
722
647
  });
723
- 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) => {
648
+ const sessionCmd = agent.command("session").description("Session lifecycle commands");
649
+ sessionCmd.command("list <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) => {
724
650
  try {
725
651
  const serverUrl = resolveServerUrl(options.server);
726
652
  const adminToken = await ensureFreshAccessToken();
727
- const agentId = (await resolveAgent(serverUrl, adminToken, agentName)).uuid;
653
+ const agentId = (await resolveAgent$1(serverUrl, adminToken, agentName)).uuid;
728
654
  const response = await cliFetch(`${serverUrl}/api/v1/agents/${agentId}/sessions${options.state ? `?state=${options.state}` : ""}`, {
729
655
  headers: { Authorization: `Bearer ${adminToken}` },
730
656
  signal: AbortSignal.timeout(1e4)
@@ -748,12 +674,11 @@ function registerAgentCommands(program) {
748
674
  fail("SESSIONS_ERROR", error instanceof Error ? error.message : String(error));
749
675
  }
750
676
  });
751
- const sessionCmd = agent.command("session").description("Session lifecycle commands");
752
677
  for (const [cmd, desc] of [["suspend", "Suspend a session"], ["terminate", "Terminate a session"]]) sessionCmd.command(`${cmd} <agent-name> <chat-id>`).description(desc).option("--server <url>", "Hub server URL").action(async (agentName, chatId, options) => {
753
678
  try {
754
679
  const serverUrl = resolveServerUrl(options.server);
755
680
  const adminToken = await ensureFreshAccessToken();
756
- const agentId = (await resolveAgent(serverUrl, adminToken, agentName)).uuid;
681
+ const agentId = (await resolveAgent$1(serverUrl, adminToken, agentName)).uuid;
757
682
  const response = await cliFetch(`${serverUrl}/api/v1/agents/${agentId}/sessions/${chatId}/${cmd}`, {
758
683
  method: "POST",
759
684
  headers: { Authorization: `Bearer ${adminToken}` },
@@ -768,7 +693,167 @@ function registerAgentCommands(program) {
768
693
  fail("SESSION_CMD_ERROR", error instanceof Error ? error.message : String(error));
769
694
  }
770
695
  });
771
- agent.command("chat <agent-name>").description("Open an interactive chat with an agent (as the current member's human agent)").option("--server <url>", "Hub server URL").action(async (agentName, options) => {
696
+ const debugCmd = agent.command("debug", { hidden: true }).description("Low-level SDK debug commands");
697
+ debugCmd.command("register").description("Register this agent and return identity info").option("--agent <name>", "Agent name on the Hub (default: first configured on this client)").action(async (options) => {
698
+ try {
699
+ success(await createSdk$1(options.agent).register());
700
+ } catch (error) {
701
+ handleSdkError$1(error);
702
+ }
703
+ });
704
+ debugCmd.command("pull").description("Pull pending messages from inbox").option("-l, --limit <number>", "Maximum entries to return", "10").option("-a, --ack", "Automatically ACK entries after pulling").option("--agent <name>", "Agent name on the Hub (default: first configured on this client)").action(async (options) => {
705
+ try {
706
+ const sdk = createSdk$1(options.agent);
707
+ const limit = parseLimit$1(options.limit, 50);
708
+ const result = await sdk.pull(limit);
709
+ if (options.ack && result.entries.length > 0) await Promise.all(result.entries.map((entry) => sdk.ack(entry.id)));
710
+ success(result);
711
+ } catch (error) {
712
+ handleSdkError$1(error);
713
+ }
714
+ });
715
+ }
716
+ //#endregion
717
+ //#region src/commands/chat.ts
718
+ function resolveLocalAgent(agentName) {
719
+ const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
720
+ const agents = loadAgents({
721
+ schema: agentConfigSchema,
722
+ agentsDir
723
+ });
724
+ const resolution = resolveSenderName({
725
+ override: agentName,
726
+ envAgentId: process.env.FIRST_TREE_HUB_AGENT_ID,
727
+ agents
728
+ });
729
+ let resolvedName;
730
+ if (resolution.kind === "ok") resolvedName = resolution.name;
731
+ else if (resolution.kind === "none") fail("MISSING_AGENT", "No agent configured. Run `first-tree-hub agent add` first.", 2);
732
+ else if (resolution.kind === "envMismatch") fail("ENV_AGENT_NOT_LOCAL", `FIRST_TREE_HUB_AGENT_ID="${resolution.envAgentId}" is not configured on this machine. Available local agents: ${resolution.available.join(", ")}. Pick one explicitly: \`first-tree-hub chat send --agent <senderName> <recipientName> "..."\`.`, 2);
733
+ else fail("AMBIGUOUS_AGENT", `Multiple agents are configured on this machine (${resolution.available.join(", ")}) and FIRST_TREE_HUB_AGENT_ID is not set, so the CLI can't tell which one is the sender. Specify it explicitly: \`first-tree-hub chat send --agent <senderName> <recipientName> "..."\` (--agent picks the SENDER; the recipient is the next positional argument).`, 2);
734
+ const cfg = agents.get(resolvedName);
735
+ if (!cfg) fail("UNKNOWN_AGENT", `Agent "${resolvedName}" not found in ${agentsDir}`, 2);
736
+ let serverUrl;
737
+ try {
738
+ serverUrl = resolveServerUrl(process.env.FIRST_TREE_HUB_SERVER_URL);
739
+ } catch (error) {
740
+ fail("MISSING_SERVER_URL", error instanceof Error ? error.message : String(error), 2);
741
+ }
742
+ return {
743
+ serverUrl,
744
+ agentId: cfg.agentId
745
+ };
746
+ }
747
+ function createSdk(agentName) {
748
+ const { serverUrl, agentId } = resolveLocalAgent(agentName);
749
+ return new FirstTreeHubSDK({
750
+ serverUrl,
751
+ getAccessToken: (opts) => ensureFreshAccessToken(opts),
752
+ agentId,
753
+ userAgent: CLI_USER_AGENT
754
+ });
755
+ }
756
+ function handleSdkError(error) {
757
+ if (error instanceof SdkError) {
758
+ const exitCode = error.statusCode === 401 ? 3 : 1;
759
+ fail(`HTTP_${error.statusCode}`, error.message, exitCode);
760
+ }
761
+ if (error instanceof TypeError && "cause" in error) fail("CONNECTION_ERROR", `Cannot connect to server: ${error.message}`, 6);
762
+ fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), 1);
763
+ }
764
+ function parseLimit(value, max) {
765
+ const limit = Number.parseInt(value, 10);
766
+ if (Number.isNaN(limit) || limit < 1 || limit > max) fail("INVALID_LIMIT", `Limit must be between 1 and ${max}.`, 2);
767
+ return limit;
768
+ }
769
+ const MAX_STDIN_BYTES = 10 * 1024 * 1024;
770
+ function readStdin() {
771
+ if (process.stdin.isTTY) return Promise.resolve(null);
772
+ return new Promise((resolve, reject) => {
773
+ const chunks = [];
774
+ let totalSize = 0;
775
+ process.stdin.on("data", (chunk) => {
776
+ totalSize += chunk.length;
777
+ if (totalSize > MAX_STDIN_BYTES) {
778
+ process.stdin.destroy();
779
+ reject(/* @__PURE__ */ new Error(`stdin exceeds ${MAX_STDIN_BYTES} bytes`));
780
+ return;
781
+ }
782
+ chunks.push(chunk);
783
+ });
784
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
785
+ process.stdin.on("error", reject);
786
+ });
787
+ }
788
+ async function resolveAgent(serverUrl, adminToken, agentName) {
789
+ const res = await cliFetch(`${serverUrl}/api/v1/me/managed-agents`, {
790
+ headers: { Authorization: `Bearer ${adminToken}` },
791
+ signal: AbortSignal.timeout(1e4)
792
+ });
793
+ if (!res.ok) fail("FETCH_ERROR", `Failed to list agents: ${res.status}`, 1);
794
+ const found = (await res.json()).find((a) => a.name === agentName || a.uuid === agentName);
795
+ if (!found) fail("NOT_FOUND", `Agent "${agentName}" not found`, 1);
796
+ return found;
797
+ }
798
+ function registerChatCommands(program) {
799
+ const chat = program.command("chat").description("Chats and messaging — list, history, send, open");
800
+ chat.command("send <target> [message]").description("Send a message to an agent or chat").option("-f, --format <format>", "Message format (text|markdown|card)", "text").option("--chat", "Treat target as chat ID instead of agent ID").option("-m, --metadata <json>", "JSON metadata to attach").option("--reply-to <messageId>", "Message ID to reply to").option("--reply-to-inbox <inboxId>", "Cross-chat reply target inbox").option("--reply-to-chat <chatId>", "Cross-chat reply target chat").option("--agent <name>", "Agent name on the Hub (default: first configured on this client)").action(async (target, message, options) => {
801
+ try {
802
+ const content = message ?? await readStdin();
803
+ if (!content) fail("NO_MESSAGE", "No message provided. Pass as argument or pipe via stdin.", 2);
804
+ let metadata;
805
+ if (options.metadata) try {
806
+ metadata = JSON.parse(options.metadata);
807
+ } catch {
808
+ fail("INVALID_METADATA", "Metadata must be valid JSON.", 2);
809
+ }
810
+ const { replyToInbox, replyToChat } = resolveReplyToFromEnv(process.env, {
811
+ replyToInbox: options.replyToInbox,
812
+ replyToChat: options.replyToChat
813
+ });
814
+ const sdk = createSdk(options.agent);
815
+ if (options.chat) success(await sdk.sendMessage(target, {
816
+ format: options.format,
817
+ content,
818
+ metadata,
819
+ inReplyTo: options.replyTo,
820
+ replyToInbox,
821
+ replyToChat
822
+ }));
823
+ else success(await sdk.sendToAgent(target, {
824
+ format: options.format,
825
+ content,
826
+ metadata,
827
+ replyToInbox,
828
+ replyToChat
829
+ }));
830
+ } catch (error) {
831
+ handleSdkError(error);
832
+ }
833
+ });
834
+ chat.command("list").description("List chats this agent participates in").option("-l, --limit <number>", "Maximum chats to return (1-100)", "20").option("--cursor <cursor>", "Pagination cursor from previous response").option("--agent <name>", "Agent name on the Hub (default: first configured on this client)").action(async (options) => {
835
+ try {
836
+ const limit = parseLimit(options.limit, 100);
837
+ success(await createSdk(options.agent).listChats({
838
+ limit,
839
+ cursor: options.cursor
840
+ }));
841
+ } catch (error) {
842
+ handleSdkError(error);
843
+ }
844
+ });
845
+ chat.command("history <chatId>").description("View message history in a chat").option("-l, --limit <number>", "Maximum messages to return (1-100)", "20").option("--cursor <cursor>", "Pagination cursor from previous response").option("--agent <name>", "Agent name on the Hub (default: first configured on this client)").action(async (chatId, options) => {
846
+ try {
847
+ const limit = parseLimit(options.limit, 100);
848
+ success(await createSdk(options.agent).listMessages(chatId, {
849
+ limit,
850
+ cursor: options.cursor
851
+ }));
852
+ } catch (error) {
853
+ handleSdkError(error);
854
+ }
855
+ });
856
+ chat.command("open <agent-name>").description("Open an interactive chat with an agent (as the current member's human agent)").option("--server <url>", "Hub server URL").action(async (agentName, options) => {
772
857
  try {
773
858
  const serverUrl = resolveServerUrl(options.server);
774
859
  const adminToken = await ensureFreshAccessToken();
@@ -857,277 +942,6 @@ function registerAgentCommands(program) {
857
942
  fail("CHAT_ERROR", error instanceof Error ? error.message : String(error));
858
943
  }
859
944
  });
860
- agent.command("register").description("Register this agent and return identity info").option("--agent <name>", "Agent name on the Hub (default: first configured on this client)").action(async (options) => {
861
- try {
862
- success(await createSdk(options.agent).register());
863
- } catch (error) {
864
- handleSdkError(error);
865
- }
866
- });
867
- agent.command("pull").description("Pull pending messages from inbox").option("-l, --limit <number>", "Maximum entries to return", "10").option("-a, --ack", "Automatically ACK entries after pulling").option("--agent <name>", "Agent name on the Hub (default: first configured on this client)").action(async (options) => {
868
- try {
869
- const sdk = createSdk(options.agent);
870
- const limit = parseLimit(options.limit, 50);
871
- const result = await sdk.pull(limit);
872
- if (options.ack && result.entries.length > 0) await Promise.all(result.entries.map((entry) => sdk.ack(entry.id)));
873
- success(result);
874
- } catch (error) {
875
- handleSdkError(error);
876
- }
877
- });
878
- }
879
- //#endregion
880
- //#region src/commands/connect.ts
881
- /** Decode a JWT payload without signature verification. For UI purposes only. */
882
- function decodeJwtPayload(token) {
883
- try {
884
- const parts = token.split(".");
885
- if (parts.length !== 3 || !parts[1]) return null;
886
- const raw = Buffer.from(parts[1], "base64url").toString();
887
- const obj = JSON.parse(raw);
888
- if (typeof obj !== "object" || obj === null) return null;
889
- return obj;
890
- } catch {
891
- return null;
892
- }
893
- }
894
- /**
895
- * Detect if the current home already holds a setup for a *different* account,
896
- * and give the operator a chance to back out before we overwrite credentials.
897
- *
898
- * Why this gate exists: running `client connect` implicitly overwrites
899
- * `~/.first-tree/hub/config/credentials.json`. Without this prompt, someone
900
- * onboarding a second account on their own machine silently logs themselves
901
- * out of the first account — they'd only notice when their "main" agents
902
- * appeared offline later. We treat single-account-per-machine as the product
903
- * default; the `FIRST_TREE_HUB_HOME` env var remains the advanced escape
904
- * hatch for power users who want parallel setups.
905
- *
906
- * The caller passes the *new* memberId directly so this gate can run BEFORE
907
- * auth. That matters for the `--token` path: connect tokens are single-use;
908
- * if we auth first and the user picks Cancel, the token is burned even
909
- * though nothing changed on disk. Decoding the connect token locally lets
910
- * us return early without spending it.
911
- *
912
- * Behavior:
913
- * - No existing credentials → proceed silently (first-time install).
914
- * - Existing credentials, same memberId → proceed silently (reconnect /
915
- * token refresh — common + safe).
916
- * - Existing credentials, memberId indeterminate → prompt with
917
- * "unknown account" label so the user can decide.
918
- * - Existing credentials, different memberId → prompt [Replace / Cancel].
919
- * Cancel prints the isolation guide and returns "cancel".
920
- */
921
- async function promptReplaceOrCancel(newMemberId, newServerUrl) {
922
- const existing = loadCredentials();
923
- if (!existing) return "proceed";
924
- const existingPayload = decodeJwtPayload(existing.accessToken);
925
- const existingMemberId = typeof existingPayload?.memberId === "string" ? existingPayload.memberId : null;
926
- if (existingMemberId && existingMemberId === newMemberId) return "proceed";
927
- const existingMember = existingMemberId ? `member ${existingMemberId.slice(0, 8)}` : "unknown account";
928
- const existingOrg = typeof existingPayload?.organizationId === "string" ? existingPayload.organizationId : null;
929
- const serviceStatus = getClientServiceStatus();
930
- const serviceLine = serviceStatus.state === "active" ? `running (${serviceStatus.detail ?? "live"})` : serviceStatus.state === "inactive" ? `installed but not running${serviceStatus.detail ? ` — ${serviceStatus.detail}` : ""}` : "not installed";
931
- print.line("\n");
932
- print.line(" ⚠️ This computer is already connected to the Hub under another account.\n\n");
933
- print.line(` Existing account: ${existingMember}\n`);
934
- if (existingOrg) print.line(` Organization: ${existingOrg.slice(0, 8)}\n`);
935
- print.line(` Server: ${existing.serverUrl}\n`);
936
- print.line(` Background service: ${serviceLine}\n\n`);
937
- print.line(" Replacing only affects THIS computer. Your agents, messages, and\n");
938
- print.line(" settings on the Hub itself are untouched.\n\n");
939
- if (await select({
940
- message: "How would you like to continue?",
941
- choices: [{
942
- name: "Replace — log out the other account and set up this one",
943
- value: "replace"
944
- }, {
945
- name: "Cancel — keep the existing setup on this computer",
946
- value: "cancel"
947
- }]
948
- }) === "cancel") {
949
- printIsolationGuide(newServerUrl);
950
- return "cancel";
951
- }
952
- return "proceed";
953
- }
954
- function printIsolationGuide(newServerUrl) {
955
- print.line("\n Cancelled. The existing account on this computer is untouched.\n\n");
956
- print.line(" To run this new account alongside it (advanced — no background service):\n\n");
957
- print.line(" export FIRST_TREE_HUB_HOME=\"$HOME/.first-tree/hub-<label>\"\n");
958
- print.line(` first-tree-hub client connect ${newServerUrl} --token <token>\n`);
959
- print.line(" first-tree-hub client start\n\n");
960
- print.line(" Notes:\n");
961
- print.line(" - Run the commands in a FRESH terminal (the isolated home must be set first).\n");
962
- print.line(" - In isolated mode the client stays online only while that terminal runs.\n");
963
- print.line(" - The main account's background service is not affected.\n\n");
964
- }
965
- /**
966
- * Authenticate via connect token — exchange for full JWT credentials.
967
- */
968
- async function authenticateWithToken(url, token) {
969
- const res = await cliFetch(`${url}/api/v1/auth/connect-token`, {
970
- method: "POST",
971
- headers: { "Content-Type": "application/json" },
972
- body: JSON.stringify({ token }),
973
- signal: AbortSignal.timeout(1e4)
974
- });
975
- if (!res.ok) fail("AUTH_ERROR", (await res.json().catch(() => ({}))).error ?? `Token exchange failed (HTTP ${res.status})`, 1);
976
- return await res.json();
977
- }
978
- /**
979
- * Authenticate via interactive username/password login.
980
- */
981
- async function authenticateInteractive(url) {
982
- print.line("\n Log in to Hub:\n");
983
- const username = await input({ message: " Username:" });
984
- const pw = await password({ message: " Password:" });
985
- const loginRes = await cliFetch(`${url}/api/v1/auth/login`, {
986
- method: "POST",
987
- headers: { "Content-Type": "application/json" },
988
- body: JSON.stringify({
989
- username,
990
- password: pw
991
- }),
992
- signal: AbortSignal.timeout(1e4)
993
- });
994
- if (!loginRes.ok) fail("AUTH_ERROR", (await loginRes.json().catch(() => ({}))).error ?? `Login failed (HTTP ${loginRes.status})`, 1);
995
- return await loginRes.json();
996
- }
997
- function registerConnectCommand(parent) {
998
- parent.command("connect <server-url>").description("Connect to a Hub server — configure, authenticate, and install the background service").option("--token <token>", "Connect token (from Hub web console) — skips interactive login").option("--no-service", "Skip background service install (runs inline until Ctrl+C)").action(async (serverUrl, options) => {
999
- try {
1000
- const url = serverUrl.replace(/\/+$/, "");
1001
- let preAuthDecided = false;
1002
- if (options.token) {
1003
- const connectPayload = decodeJwtPayload(options.token);
1004
- const newMemberId = typeof connectPayload?.memberId === "string" ? connectPayload.memberId : null;
1005
- if (newMemberId !== null) {
1006
- if (await promptReplaceOrCancel(newMemberId, url) === "cancel") return;
1007
- preAuthDecided = true;
1008
- }
1009
- }
1010
- const tokens = options.token ? await authenticateWithToken(url, options.token) : await authenticateInteractive(url);
1011
- if (!preAuthDecided) {
1012
- const newPayload = decodeJwtPayload(tokens.accessToken);
1013
- const newMemberId = typeof newPayload?.memberId === "string" ? newPayload.memberId : null;
1014
- if (newMemberId !== null) {
1015
- if (await promptReplaceOrCancel(newMemberId, url) === "cancel") return;
1016
- }
1017
- }
1018
- setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", url);
1019
- print.line(`\n \u2713 Server configured: ${url}\n`);
1020
- saveCredentials({
1021
- ...tokens,
1022
- serverUrl: url
1023
- });
1024
- print.line(" ✓ Authenticated\n");
1025
- resetConfig();
1026
- resetConfigMeta();
1027
- const config = await initConfig({
1028
- schema: clientConfigSchema,
1029
- role: "client"
1030
- });
1031
- print.line(` \u2713 Connected as this computer (id: ${config.client.id})\n`);
1032
- if (options.service !== false && isServiceSupported()) {
1033
- const info = installClientService();
1034
- print.line(` \u2713 Installed as a background service (${info.platform}) — you can close this terminal\n\n`);
1035
- print.line(` Unit: ${info.unitPath}\n`);
1036
- print.line(` Logs: ${info.logDir}\n`);
1037
- if (info.state === "active" && info.detail) print.line(` State: running (${info.detail})\n`);
1038
- print.line("\n");
1039
- return;
1040
- }
1041
- if (options.service === false) print.line(" (--no-service) running inline — Ctrl+C to stop\n");
1042
- else print.line(` Background service not supported on ${process.platform}; running inline.\n`);
1043
- const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
1044
- try {
1045
- await migrateLocalAgentDirs({
1046
- agentsDir,
1047
- workspacesDir: join(DEFAULT_DATA_DIR, "workspaces"),
1048
- sessionsDir: join(DEFAULT_DATA_DIR, "sessions"),
1049
- resolver: createApiNameResolver(config.server.url, () => ensureFreshAccessToken())
1050
- });
1051
- } catch (err) {
1052
- const msg = err instanceof Error ? err.message : String(err);
1053
- print.status("⚠️", `agent-dir migration skipped: ${msg}`);
1054
- }
1055
- let probedCapabilities = null;
1056
- try {
1057
- const accessToken = await ensureFreshAccessToken();
1058
- probedCapabilities = await probeCapabilities();
1059
- await reconcileLocalRuntimeProviders({
1060
- serverUrl: config.server.url,
1061
- accessToken,
1062
- agentsDir,
1063
- log: (level, msg) => print.status(level === "warn" ? "⚠️" : "•", msg)
1064
- });
1065
- } catch (err) {
1066
- const msg = err instanceof Error ? err.message : String(err);
1067
- print.status("⚠️", `runtime-provider reconcile skipped: ${msg}`);
1068
- }
1069
- const agents = loadAgents({
1070
- schema: agentConfigSchema,
1071
- agentsDir
1072
- });
1073
- const runtime = new ClientRuntime(config.server.url, config.client.id, {
1074
- currentVersion: COMMAND_VERSION,
1075
- update: {
1076
- updateConfig: config.update,
1077
- prompt: promptUpdate,
1078
- executeUpdate: createExecuteUpdate({ managed: false })
1079
- }
1080
- });
1081
- for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
1082
- await runtime.start();
1083
- if (probedCapabilities) try {
1084
- const accessToken = await ensureFreshAccessToken();
1085
- await uploadClientCapabilities({
1086
- serverUrl: config.server.url,
1087
- accessToken,
1088
- clientId: config.client.id,
1089
- capabilities: probedCapabilities
1090
- });
1091
- } catch (err) {
1092
- const msg = err instanceof Error ? err.message : String(err);
1093
- print.status("⚠️", `capabilities upload skipped: ${msg}`);
1094
- }
1095
- runtime.watchAgentsDir(agentsDir);
1096
- const shutdown = async () => {
1097
- print.line("\n Shutting down...\n");
1098
- runtime.unwatchAgentsDir();
1099
- await runtime.stop();
1100
- process.exit(0);
1101
- };
1102
- process.on("SIGINT", () => void shutdown());
1103
- process.on("SIGTERM", () => void shutdown());
1104
- await new Promise(() => {});
1105
- } catch (error) {
1106
- if (error.name === "ExitPromptError") {
1107
- print.line("\n Cancelled.\n");
1108
- return;
1109
- }
1110
- if (error instanceof ClientUserMismatchError) {
1111
- print.line("\n");
1112
- print.line(" ⚠️ This client.yaml is owned by a different user.\n");
1113
- print.line(" Run `first-tree-hub client claim --confirm` to transfer ownership\n");
1114
- print.line(" to your account. The previous owner's agents will be unpinned\n");
1115
- print.line(" from this machine.\n\n");
1116
- process.exit(1);
1117
- }
1118
- if (error instanceof ClientOrgMismatchError) await handleClientOrgMismatch(error, {
1119
- managed: false,
1120
- configDir: DEFAULT_CONFIG_DIR,
1121
- rerunCommand: `first-tree-hub client connect ${serverUrl}${options.token ? " --token <token>" : ""}${options.service === false ? " --no-service" : ""}`
1122
- });
1123
- const msg = error instanceof Error ? error.message : String(error);
1124
- print.line(` Error: ${msg}\n`);
1125
- process.exit(1);
1126
- } finally {
1127
- resetConfig();
1128
- resetConfigMeta();
1129
- }
1130
- });
1131
945
  }
1132
946
  //#endregion
1133
947
  //#region src/commands/client.ts
@@ -1142,7 +956,6 @@ function isWslDbusOvermount(reason) {
1142
956
  }
1143
957
  function registerClientCommands(program) {
1144
958
  const client = program.command("client").description("Client runtime — connect agents to the server");
1145
- registerConnectCommand(client);
1146
959
  client.command("start").description("Start client — connect all configured agents to the server").option("--no-interactive", "Skip interactive prompts (for Docker/CI)").option("--foreground", "Run inline instead of delegating to the background service (for debugging)").action(async (options) => {
1147
960
  try {
1148
961
  const isSupervisorChild = options.interactive === false && process.env.FIRST_TREE_HUB_SERVICE_MODE === "1";
@@ -1362,7 +1175,7 @@ function registerClientCommands(program) {
1362
1175
  }
1363
1176
  if (getClientServiceStatus().state === "not-installed") {
1364
1177
  print.line("\n No background service installed.\n");
1365
- print.line(" Run `first-tree-hub client connect <url>` first.\n\n");
1178
+ print.line(" Run `first-tree-hub connect <token>` first.\n\n");
1366
1179
  process.exit(1);
1367
1180
  }
1368
1181
  const res = restartClientService();
@@ -1381,7 +1194,7 @@ function registerClientCommands(program) {
1381
1194
  const tail = svc.platform === "systemd" ? ` (logs: journalctl --user -u ${svc.label.replace(/\.service$/, "")} -f)` : "";
1382
1195
  if (svc.state === "active") print.line(` Service: ✓ running (${svc.platform}${svc.detail ? `, ${svc.detail}` : ""})${tail}\n`);
1383
1196
  else if (svc.state === "inactive") print.line(` Service: ✗ stopped (${svc.platform}${svc.detail ? `, ${svc.detail}` : ""})\n`);
1384
- else if (svc.state === "not-installed") print.line(" Service: not installed — run `first-tree-hub client connect <url>`\n");
1197
+ else if (svc.state === "not-installed") print.line(" Service: not installed — run `first-tree-hub connect <token>`\n");
1385
1198
  else print.line(` Service: unknown (${svc.platform}${svc.detail ? `, ${svc.detail}` : ""})\n`);
1386
1199
  } else print.line(` Service: not supported on ${process.platform} (runs inline)\n`);
1387
1200
  const clientYaml = join(DEFAULT_CONFIG_DIR, "client.yaml");
@@ -1395,7 +1208,7 @@ function registerClientCommands(program) {
1395
1208
  const msg = err instanceof Error ? err.message : String(err);
1396
1209
  print.line(` Hub: (could not read ${clientYaml}: ${msg.slice(0, 60)})\n`);
1397
1210
  }
1398
- else print.line(" Hub: (not configured — run `first-tree-hub client connect <url>`)\n");
1211
+ else print.line(" Hub: (not configured — run `first-tree-hub connect <token>`)\n");
1399
1212
  const creds = loadCredentials();
1400
1213
  if (creds) {
1401
1214
  const exp = decodeJwtExpSeconds(creds.refreshToken);
@@ -1413,7 +1226,7 @@ function registerClientCommands(program) {
1413
1226
  print.line(` Auth: ✓ refresh token valid for ~${days}d\n`);
1414
1227
  }
1415
1228
  }
1416
- } else print.line(" Auth: (no credentials — run `first-tree-hub client connect <url>`)\n");
1229
+ } else print.line(" Auth: (no credentials — run `first-tree-hub connect <token>`)\n");
1417
1230
  const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
1418
1231
  try {
1419
1232
  const agents = loadAgents({
@@ -1431,7 +1244,7 @@ function registerClientCommands(program) {
1431
1244
  print.line(" Agents: (no agents directory)\n\n");
1432
1245
  }
1433
1246
  });
1434
- client.command("hub-list").description("List clients on the Hub server").option("--server <url>", "Hub server URL").action(async (options) => {
1247
+ client.command("list").description("List clients on the Hub server").option("--server <url>", "Hub server URL").action(async (options) => {
1435
1248
  try {
1436
1249
  const serverUrl = resolveServerUrl(options.server);
1437
1250
  const token = await ensureFreshAccessToken();
@@ -1556,7 +1369,7 @@ function registerClientCommands(program) {
1556
1369
  resetConfigMeta();
1557
1370
  }
1558
1371
  });
1559
- client.command("hub-disconnect <clientId>").description("Force-disconnect a client from the Hub server").option("--server <url>", "Hub server URL").action(async (clientId, options) => {
1372
+ client.command("disconnect <clientId>").description("Force-disconnect a client from the Hub server").option("--server <url>", "Hub server URL").action(async (clientId, options) => {
1560
1373
  try {
1561
1374
  const serverUrl = resolveServerUrl(options.server);
1562
1375
  const token = await ensureFreshAccessToken();
@@ -1571,109 +1384,47 @@ function registerClientCommands(program) {
1571
1384
  fail("DISCONNECT_ERROR", error instanceof Error ? error.message : String(error));
1572
1385
  }
1573
1386
  });
1574
- }
1575
- function timeSince(isoDate) {
1576
- const ms = Date.now() - new Date(isoDate).getTime();
1577
- const seconds = Math.floor(ms / 1e3);
1578
- if (seconds < 60) return `${seconds}s`;
1579
- const minutes = Math.floor(seconds / 60);
1580
- if (minutes < 60) return `${minutes}m`;
1581
- const hours = Math.floor(minutes / 60);
1582
- if (hours < 24) return `${hours}h ${minutes % 60}m`;
1583
- return `${Math.floor(hours / 24)}d ${hours % 24}h`;
1584
- }
1585
- /** Read a `dot.path.like.this` from a parsed YAML object, returning string | null. */
1586
- function getNested(obj, path) {
1587
- let cur = obj;
1588
- for (const part of path.split(".")) {
1589
- if (cur === null || cur === void 0 || typeof cur !== "object") return null;
1590
- cur = cur[part];
1591
- }
1592
- return typeof cur === "string" ? cur : null;
1593
- }
1594
- /**
1595
- * Pull the `exp` claim (in seconds since epoch) out of a JWT without
1596
- * verifying the signature — the `client status` auth-health line just
1597
- * needs the wall-clock countdown, not a trust decision. Returns null
1598
- * for malformed tokens so the caller can render a friendly fallback
1599
- * instead of crashing.
1600
- */
1601
- function decodeJwtExpSeconds(token) {
1602
- const parts = token.split(".");
1603
- if (parts.length < 2 || !parts[1]) return null;
1604
- try {
1605
- const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
1606
- const pad = b64.length % 4 === 0 ? "" : "=".repeat(4 - b64.length % 4);
1607
- const payload = JSON.parse(Buffer.from(b64 + pad, "base64").toString("utf-8"));
1608
- return typeof payload.exp === "number" ? payload.exp : null;
1609
- } catch {
1610
- return null;
1611
- }
1612
- }
1613
- //#endregion
1614
- //#region src/commands/config.ts
1615
- function resolveConfigPath(flags) {
1616
- if (flags.agent) return {
1617
- path: join(DEFAULT_CONFIG_DIR, "agents", flags.agent, "agent.yaml"),
1618
- schema: agentConfigSchema
1619
- };
1620
- if (flags.client) return {
1621
- path: join(DEFAULT_CONFIG_DIR, "client.yaml"),
1622
- schema: clientConfigSchema
1623
- };
1624
- return {
1625
- path: join(DEFAULT_CONFIG_DIR, "server.yaml"),
1626
- schema: serverConfigSchema
1627
- };
1628
- }
1629
- function addScopeOptions(cmd) {
1630
- return cmd.option("-s, --server", "Server config scope (default)").option("-c, --client", "Client config scope").option("-a, --agent <name>", "Agent config scope");
1631
- }
1632
- function registerConfigCommands(program) {
1633
- const config = program.command("config").description("Configuration management");
1634
- config.command("setup").description("Interactive configuration wizard").option("-s, --server", "Configure server (default)").option("-c, --client", "Configure client").action(async (flags) => {
1635
- try {
1636
- await promptMissingFields({
1637
- schema: flags.client ? clientConfigSchema : serverConfigSchema,
1638
- role: flags.client ? "client" : "server"
1639
- });
1640
- print.line("\n Configuration saved.\n");
1641
- } catch (error) {
1642
- if (error.name === "ExitPromptError") {
1643
- print.line("\n Cancelled.\n");
1387
+ const config = client.command("config").description("View and modify this machine's client.yaml");
1388
+ const clientYamlPath = () => join(DEFAULT_CONFIG_DIR, "client.yaml");
1389
+ const clientSchema = clientConfigSchema;
1390
+ config.command("show [key]").description("Show client.yaml — print all values, or a single key with dot-notation").option("--show-secrets", "Show secret values in plaintext").action((key, flags) => {
1391
+ const path = clientYamlPath();
1392
+ if (key) {
1393
+ const value = getConfigValue(path, key);
1394
+ if (value === void 0) {
1395
+ print.line(` ${key}: (not set)\n`);
1644
1396
  return;
1645
1397
  }
1646
- throw error;
1398
+ const display = isSecretField(clientSchema, key) && !flags.showSecrets ? "***" : String(value);
1399
+ print.line(` ${key}: ${display}\n`);
1400
+ return;
1647
1401
  }
1402
+ const values = readConfigFile(path);
1403
+ if (Object.keys(values).length === 0) {
1404
+ print.line(` No config found at ${path}\n`);
1405
+ return;
1406
+ }
1407
+ print.line(`\n Config: ${path}\n\n`);
1408
+ printFlat(values, clientSchema, "", flags.showSecrets ?? false);
1409
+ print.line("\n");
1648
1410
  });
1649
- addScopeOptions(config.command("set").description("Set a config value")).argument("<key>", "Config key (dot notation, e.g. database.url)").argument("<value>", "Config value").action((key, value, flags) => {
1650
- const { path } = resolveConfigPath(flags);
1411
+ config.command("set <key> <value>").description("Set a value in client.yaml (dot-notation)").action((key, value) => {
1651
1412
  let parsed = value;
1652
1413
  if (value === "true") parsed = true;
1653
1414
  else if (value === "false") parsed = false;
1654
1415
  else if (/^\d+$/.test(value)) parsed = Number(value);
1416
+ const path = clientYamlPath();
1655
1417
  setConfigValue(path, key, parsed);
1656
1418
  print.line(` Set ${key} in ${path}\n`);
1657
1419
  });
1658
- 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) => {
1659
- const { path, schema } = resolveConfigPath(flags);
1660
- const value = getConfigValue(path, key);
1661
- if (value === void 0) print.line(` ${key}: (not set)\n`);
1662
- else {
1663
- const display = isSecretField(schema, key) && !flags.showSecrets ? "***" : String(value);
1664
- print.line(` ${key}: ${display}\n`);
1665
- }
1666
- });
1667
- addScopeOptions(config.command("list").description("List all config values")).option("--show-secrets", "Show secret values in plaintext").action((flags) => {
1668
- const { path, schema } = resolveConfigPath(flags);
1669
- const values = readConfigFile(path);
1670
- if (Object.keys(values).length === 0) {
1671
- print.line(` No config found at ${path}\n`);
1420
+ config.command("get <key>").description("Get a value from client.yaml (alias for `show <key>`)").option("--show-secrets", "Show secret values in plaintext").action((key, flags) => {
1421
+ const value = getConfigValue(clientYamlPath(), key);
1422
+ if (value === void 0) {
1423
+ print.line(` ${key}: (not set)\n`);
1672
1424
  return;
1673
1425
  }
1674
- print.line(`\n Config: ${path}\n\n`);
1675
- printFlat(values, schema, "", flags.showSecrets ?? false);
1676
- print.line("\n");
1426
+ const display = isSecretField(clientSchema, key) && !flags.showSecrets ? "***" : String(value);
1427
+ print.line(` ${key}: ${display}\n`);
1677
1428
  });
1678
1429
  }
1679
1430
  function printFlat(obj, schema, prefix, showSecrets) {
@@ -1686,7 +1437,6 @@ function printFlat(obj, schema, prefix, showSecrets) {
1686
1437
  }
1687
1438
  }
1688
1439
  }
1689
- /** Check if a dot-path corresponds to a secret field in the schema. */
1690
1440
  function isSecretField(schema, dotPath) {
1691
1441
  const parts = dotPath.split(".");
1692
1442
  let current = schema;
@@ -1703,18 +1453,56 @@ function isSecretField(schema, dotPath) {
1703
1453
  }
1704
1454
  return false;
1705
1455
  }
1456
+ function timeSince(isoDate) {
1457
+ const ms = Date.now() - new Date(isoDate).getTime();
1458
+ const seconds = Math.floor(ms / 1e3);
1459
+ if (seconds < 60) return `${seconds}s`;
1460
+ const minutes = Math.floor(seconds / 60);
1461
+ if (minutes < 60) return `${minutes}m`;
1462
+ const hours = Math.floor(minutes / 60);
1463
+ if (hours < 24) return `${hours}h ${minutes % 60}m`;
1464
+ return `${Math.floor(hours / 24)}d ${hours % 24}h`;
1465
+ }
1466
+ /** Read a `dot.path.like.this` from a parsed YAML object, returning string | null. */
1467
+ function getNested(obj, path) {
1468
+ let cur = obj;
1469
+ for (const part of path.split(".")) {
1470
+ if (cur === null || cur === void 0 || typeof cur !== "object") return null;
1471
+ cur = cur[part];
1472
+ }
1473
+ return typeof cur === "string" ? cur : null;
1474
+ }
1475
+ /**
1476
+ * Pull the `exp` claim (in seconds since epoch) out of a JWT without
1477
+ * verifying the signature — the `client status` auth-health line just
1478
+ * needs the wall-clock countdown, not a trust decision. Returns null
1479
+ * for malformed tokens so the caller can render a friendly fallback
1480
+ * instead of crashing.
1481
+ */
1482
+ function decodeJwtExpSeconds(token) {
1483
+ const parts = token.split(".");
1484
+ if (parts.length < 2 || !parts[1]) return null;
1485
+ try {
1486
+ const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
1487
+ const pad = b64.length % 4 === 0 ? "" : "=".repeat(4 - b64.length % 4);
1488
+ const payload = JSON.parse(Buffer.from(b64 + pad, "base64").toString("utf-8"));
1489
+ return typeof payload.exp === "number" ? payload.exp : null;
1490
+ } catch {
1491
+ return null;
1492
+ }
1493
+ }
1706
1494
  //#endregion
1707
1495
  //#region src/commands/onboard.ts
1708
1496
  async function promptMissing(args) {
1709
1497
  if (!args.server) try {
1710
- const { resolveServerUrl } = await import("../bootstrap-BCZC1ki6.mjs").then((n) => n.r);
1498
+ const { resolveServerUrl } = await import("../bootstrap-Cya2OoHz.mjs").then((n) => n.r);
1711
1499
  resolveServerUrl();
1712
1500
  } catch {
1713
1501
  args.server = await input({ message: "Hub server URL:" });
1714
1502
  saveOnboardState(args);
1715
1503
  }
1716
- const { loadCredentials } = await import("../bootstrap-BCZC1ki6.mjs").then((n) => n.r);
1717
- if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub client connect <server-url>` before onboarding.");
1504
+ const { loadCredentials } = await import("../bootstrap-Cya2OoHz.mjs").then((n) => n.r);
1505
+ if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub connect <token>` before onboarding.");
1718
1506
  if (!args.id) {
1719
1507
  args.id = await input({ message: "Agent ID:" });
1720
1508
  saveOnboardState(args);
@@ -1947,7 +1735,7 @@ function registerServerCommands(program) {
1947
1735
  process.exit(1);
1948
1736
  }
1949
1737
  });
1950
- server.command("db:migrate").description("Run database migrations").action(async () => {
1738
+ server.command("migrate").description("Run database migrations").action(async () => {
1951
1739
  try {
1952
1740
  const tableCount = await runMigrations((await initConfig({
1953
1741
  schema: serverConfigSchema,
@@ -1960,7 +1748,7 @@ function registerServerCommands(program) {
1960
1748
  process.exit(1);
1961
1749
  }
1962
1750
  });
1963
- 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) => {
1751
+ server.command("admin").description("Admin user management").command("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) => {
1964
1752
  try {
1965
1753
  const result = await createOwner((await initConfig({
1966
1754
  schema: serverConfigSchema,
@@ -2039,7 +1827,7 @@ function registerUpdateCommand(program) {
2039
1827
  const svc = getClientServiceStatus();
2040
1828
  if (svc.state === "not-installed") {
2041
1829
  print.line(" No background service installed — nothing to restart.\n");
2042
- print.line(" Run `first-tree-hub client connect <url>` to set one up.\n\n");
1830
+ print.line(" Run `first-tree-hub connect <token>` to set one up.\n\n");
2043
1831
  return;
2044
1832
  }
2045
1833
  try {
@@ -2085,7 +1873,7 @@ registerSaaSConnectCommand(program);
2085
1873
  registerServerCommands(program);
2086
1874
  registerClientCommands(program);
2087
1875
  registerAgentCommands(program);
2088
- registerConfigCommands(program);
1876
+ registerChatCommands(program);
2089
1877
  registerUpdateCommand(program);
2090
1878
  registerOnboardCommand(program);
2091
1879
  registerOrgCommands(program);