@agent-team-foundation/first-tree-hub 0.5.0 → 0.6.1

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,11 +1,11 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-twds-ZHy.mjs";
2
2
  import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
- import { randomBytes } from "node:crypto";
5
- import { execSync } from "node:child_process";
6
4
  import { z } from "zod";
7
5
  import { parse, stringify } from "yaml";
6
+ import { randomBytes } from "node:crypto";
8
7
  import { homedir } from "node:os";
8
+ import { execSync } from "node:child_process";
9
9
  //#region ../shared/dist/config/index.mjs
10
10
  /** Declare a config field with a Zod schema and optional metadata. */
11
11
  function field(schema, options) {
@@ -29,7 +29,7 @@ function defineConfig(shape) {
29
29
  }
30
30
  const agentConfigSchema = defineConfig({
31
31
  token: field(z.string(), { secret: true }),
32
- type: field(z.string().default("claude-code")),
32
+ runtime: field(z.string().default("claude-code")),
33
33
  concurrency: field(z.number().int().positive().default(5)),
34
34
  session: {
35
35
  idle_timeout: field(z.number().int().positive().default(300)),
@@ -458,10 +458,6 @@ const serverConfigSchema = defineConfig({
458
458
  branch: field(z.string().default("main"))
459
459
  }),
460
460
  github: {
461
- token: field(z.string().optional(), {
462
- env: "FIRST_TREE_HUB_GITHUB_TOKEN",
463
- secret: true
464
- }),
465
461
  webhookSecret: field(z.string().optional(), {
466
462
  env: "FIRST_TREE_HUB_GITHUB_WEBHOOK_SECRET",
467
463
  secret: true
@@ -488,11 +484,18 @@ const serverConfigSchema = defineConfig({
488
484
  var bootstrap_exports = /* @__PURE__ */ __exportAll({
489
485
  bootstrapToken: () => bootstrapToken,
490
486
  checkBootstrapStatus: () => checkBootstrapStatus,
487
+ ensureFreshAdminToken: () => ensureFreshAdminToken,
491
488
  getGitHubToken: () => getGitHubToken,
492
489
  getGitHubUsername: () => getGitHubUsername,
490
+ loadAgentTokenByName: () => loadAgentTokenByName,
491
+ loadCredentials: () => loadCredentials,
492
+ maskToken: () => maskToken,
493
493
  resolveAgentToken: () => resolveAgentToken,
494
- resolveServerUrl: () => resolveServerUrl
494
+ resolveServerUrl: () => resolveServerUrl,
495
+ saveAgentConfig: () => saveAgentConfig,
496
+ saveCredentials: () => saveCredentials
495
497
  });
498
+ const CREDENTIALS_PATH = join(DEFAULT_CONFIG_DIR, "credentials.json");
496
499
  /**
497
500
  * Get the current GitHub username from `gh auth status`.
498
501
  */
@@ -522,12 +525,12 @@ function getGitHubToken() {
522
525
  */
523
526
  function resolveServerUrl(flagValue) {
524
527
  if (flagValue) return flagValue;
525
- if (process.env.FIRST_TREE_HUB_SERVER) return process.env.FIRST_TREE_HUB_SERVER;
528
+ if (process.env.FIRST_TREE_HUB_SERVER_URL) return process.env.FIRST_TREE_HUB_SERVER_URL;
526
529
  try {
527
530
  const config = getClientConfig();
528
531
  if (config.server?.url) return config.server.url;
529
532
  } catch {}
530
- throw new Error("Server URL not configured.\n Provide via: --server <url>, FIRST_TREE_HUB_SERVER env var, or\n first-tree-hub config set -c server.url <url>");
533
+ throw new Error("Server URL not configured.\n Provide via: --server <url>, FIRST_TREE_HUB_SERVER_URL env var, or\n first-tree-hub config set -c server.url <url>");
531
534
  }
532
535
  /**
533
536
  * Bootstrap a token for an agent using GitHub identity.
@@ -553,29 +556,109 @@ async function bootstrapToken(serverUrl, agentName, options = {}) {
553
556
  throw new Error(`Bootstrap failed for "${agentName}": ${msg}`);
554
557
  }
555
558
  const data = await res.json();
556
- if (options.saveTo === "agent" || !options.saveTo) {
559
+ const isHuman = options.type === "human";
560
+ if ((options.saveTo === "agent" || !options.saveTo) && !isHuman) {
557
561
  const configDir = join(DEFAULT_CONFIG_DIR, "agents", agentName);
558
562
  const configPath = `${configDir}/agent.yaml`;
559
563
  mkdirSync(configDir, {
560
564
  recursive: true,
561
565
  mode: 448
562
566
  });
563
- writeFileSync(configPath, `token: "${data.token}"\ntype: claude-code\n`, { mode: 384 });
567
+ writeFileSync(configPath, `token: "${data.token}"\nruntime: claude-code\n`, { mode: 384 });
564
568
  chmodSync(configDir, 448);
565
- } else if (options.saveTo) {
569
+ } else if (options.saveTo && options.saveTo !== "agent") {
566
570
  mkdirSync(dirname(options.saveTo), { recursive: true });
567
571
  writeFileSync(options.saveTo, data.token, { mode: 384 });
568
572
  }
569
573
  return data;
570
574
  }
571
575
  /**
572
- * Resolve agent token from FIRST_TREE_HUB_TOKEN env var.
573
- * Throws if not set.
576
+ * Load an agent's token from `~/.first-tree-hub/agents/<agentName>/agent.yaml`.
577
+ * Returns null if the file is missing or has no token.
578
+ */
579
+ function loadAgentTokenByName(agentName) {
580
+ const configPath = join(DEFAULT_CONFIG_DIR, "agents", agentName, "agent.yaml");
581
+ if (!existsSync(configPath)) return null;
582
+ try {
583
+ const raw = parse(readFileSync(configPath, "utf-8"));
584
+ if (typeof raw.token === "string" && raw.token.length > 0) return raw.token;
585
+ return null;
586
+ } catch {
587
+ return null;
588
+ }
589
+ }
590
+ /**
591
+ * Resolve agent token with the following precedence:
592
+ * 1. FIRST_TREE_HUB_AGENT_TOKEN env var (explicit token; runtime or manual export)
593
+ * 2. FIRST_TREE_HUB_AGENT env var → lookup in ~/.first-tree-hub/agents/<name>/agent.yaml
594
+ * Throws if neither is configured or the named agent has no stored token.
574
595
  */
575
596
  function resolveAgentToken() {
576
- const token = process.env.FIRST_TREE_HUB_TOKEN;
577
- if (!token) throw new Error("FIRST_TREE_HUB_TOKEN environment variable is required.");
578
- return token;
597
+ const token = process.env.FIRST_TREE_HUB_AGENT_TOKEN;
598
+ if (token) return token;
599
+ const agentName = process.env.FIRST_TREE_HUB_AGENT;
600
+ if (agentName) {
601
+ const loaded = loadAgentTokenByName(agentName);
602
+ if (loaded) return loaded;
603
+ throw new Error(`Agent "${agentName}" has no token in ${join(DEFAULT_CONFIG_DIR, "agents", agentName)}/agent.yaml.\n Verify the agent exists locally or set FIRST_TREE_HUB_AGENT_TOKEN explicitly.`);
604
+ }
605
+ throw new Error("No agent token configured.\n Set FIRST_TREE_HUB_AGENT_TOKEN directly, or\n set FIRST_TREE_HUB_AGENT=<agentName> to use a stored agent config.");
606
+ }
607
+ /**
608
+ * Ensure the persisted access token is fresh. Call before any admin API request
609
+ * when using persisted credentials. Returns the (possibly refreshed) access token.
610
+ */
611
+ async function ensureFreshAdminToken() {
612
+ const envToken = process.env.FIRST_TREE_HUB_ADMIN_TOKEN;
613
+ if (envToken) return envToken;
614
+ const creds = loadCredentials();
615
+ if (!creds) throw new Error("No credentials found.\n Run: first-tree-hub connect <server-url>\n Or set FIRST_TREE_HUB_ADMIN_TOKEN environment variable.");
616
+ if (!isTokenExpired(creds.accessToken)) return creds.accessToken;
617
+ const res = await fetch(`${creds.serverUrl}/api/v1/auth/refresh`, {
618
+ method: "POST",
619
+ headers: { "Content-Type": "application/json" },
620
+ body: JSON.stringify({ refreshToken: creds.refreshToken }),
621
+ signal: AbortSignal.timeout(1e4)
622
+ });
623
+ if (!res.ok) throw new Error("Access token expired and refresh failed.\n Run: first-tree-hub connect <server-url>");
624
+ const data = await res.json();
625
+ saveCredentials({
626
+ ...creds,
627
+ accessToken: data.accessToken
628
+ });
629
+ return data.accessToken;
630
+ }
631
+ /** Check if a JWT access token is expired (with 30s margin). */
632
+ function isTokenExpired(token) {
633
+ try {
634
+ const parts = token.split(".");
635
+ if (parts.length !== 3 || !parts[1]) return true;
636
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
637
+ if (!payload.exp) return false;
638
+ return payload.exp * 1e3 < Date.now() - 3e4;
639
+ } catch {
640
+ return true;
641
+ }
642
+ }
643
+ /** Persist credentials to disk. */
644
+ function saveCredentials(creds) {
645
+ mkdirSync(dirname(CREDENTIALS_PATH), {
646
+ recursive: true,
647
+ mode: 448
648
+ });
649
+ writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 384 });
650
+ }
651
+ /**
652
+ * Load persisted credentials saved by `connect` command.
653
+ */
654
+ function loadCredentials() {
655
+ try {
656
+ const data = JSON.parse(readFileSync(CREDENTIALS_PATH, "utf-8"));
657
+ if (data.accessToken && data.refreshToken && data.serverUrl) return data;
658
+ return null;
659
+ } catch {
660
+ return null;
661
+ }
579
662
  }
580
663
  /**
581
664
  * Check if an agent exists and is synced.
@@ -589,5 +672,22 @@ async function checkBootstrapStatus(serverUrl, agentName) {
589
672
  }
590
673
  return await res.json();
591
674
  }
675
+ /**
676
+ * Write agent config (token + runtime) to disk.
677
+ * Used by `agent create`, `agent add`, bootstrap, and server-pushed provisioning.
678
+ */
679
+ function saveAgentConfig(agentName, token, runtime) {
680
+ const agentDir = join(DEFAULT_CONFIG_DIR, "agents", agentName);
681
+ mkdirSync(agentDir, {
682
+ recursive: true,
683
+ mode: 448
684
+ });
685
+ writeFileSync(join(agentDir, "agent.yaml"), `token: "${token}"\nruntime: ${runtime}\n`, { mode: 384 });
686
+ return agentDir;
687
+ }
688
+ /** Mask a token for display: show first 6 + last 2 chars. */
689
+ function maskToken(token) {
690
+ return token.length > 8 ? `${token.slice(0, 6)}***${token.slice(-2)}` : "***";
691
+ }
592
692
  //#endregion
593
- export { setConfigValue as S, readConfigFile as _, getGitHubUsername as a, resolveConfigReadonly as b, DEFAULT_CONFIG_DIR as c, agentConfigSchema as d, clientConfigSchema as f, loadAgents as g, initConfig as h, getGitHubToken as i, DEFAULT_DATA_DIR as l, getConfigValue as m, bootstrap_exports as n, resolveAgentToken as o, collectMissingPrompts as p, checkBootstrapStatus as r, resolveServerUrl as s, bootstrapToken as t, DEFAULT_HOME_DIR as u, resetConfig as v, serverConfigSchema as x, resetConfigMeta as y };
693
+ export { resetConfig as C, setConfigValue as D, serverConfigSchema as E, readConfigFile as S, resolveConfigReadonly as T, clientConfigSchema as _, getGitHubToken as a, initConfig as b, maskToken as c, saveAgentConfig as d, saveCredentials as f, agentConfigSchema as g, DEFAULT_HOME_DIR as h, ensureFreshAdminToken as i, resolveAgentToken as l, DEFAULT_DATA_DIR as m, bootstrap_exports as n, getGitHubUsername as o, DEFAULT_CONFIG_DIR as p, checkBootstrapStatus as r, loadAgentTokenByName as s, bootstrapToken as t, resolveServerUrl as u, collectMissingPrompts as v, resetConfigMeta as w, loadAgents as x, getConfigValue as y };