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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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,17 @@ 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
+ loadCredentials: () => loadCredentials,
491
+ maskToken: () => maskToken,
493
492
  resolveAgentToken: () => resolveAgentToken,
494
- resolveServerUrl: () => resolveServerUrl
493
+ resolveServerUrl: () => resolveServerUrl,
494
+ saveAgentConfig: () => saveAgentConfig,
495
+ saveCredentials: () => saveCredentials
495
496
  });
497
+ const CREDENTIALS_PATH = join(DEFAULT_CONFIG_DIR, "credentials.json");
496
498
  /**
497
499
  * Get the current GitHub username from `gh auth status`.
498
500
  */
@@ -532,7 +534,7 @@ function resolveServerUrl(flagValue) {
532
534
  /**
533
535
  * Bootstrap a token for an agent using GitHub identity.
534
536
  */
535
- async function bootstrapToken(serverUrl, agentId, options = {}) {
537
+ async function bootstrapToken(serverUrl, agentName, options = {}) {
536
538
  const githubToken = getGitHubToken();
537
539
  const body = { name: "bootstrap" };
538
540
  if (options.type) body.type = options.type;
@@ -540,7 +542,7 @@ async function bootstrapToken(serverUrl, agentId, options = {}) {
540
542
  if (options.delegateMention) body.delegateMention = options.delegateMention;
541
543
  if (options.profile) body.profile = options.profile;
542
544
  if (options.metadata) body.metadata = options.metadata;
543
- const res = await fetch(`${serverUrl}/api/v1/bootstrap/${encodeURIComponent(agentId)}/token`, {
545
+ const res = await fetch(`${serverUrl}/api/v1/bootstrap/${encodeURIComponent(agentName)}/token`, {
544
546
  method: "POST",
545
547
  headers: {
546
548
  "X-GitHub-Token": githubToken,
@@ -550,19 +552,20 @@ async function bootstrapToken(serverUrl, agentId, options = {}) {
550
552
  });
551
553
  if (!res.ok) {
552
554
  const msg = (await res.json().catch(() => ({}))).error ?? `HTTP ${res.status}`;
553
- throw new Error(`Bootstrap failed for "${agentId}": ${msg}`);
555
+ throw new Error(`Bootstrap failed for "${agentName}": ${msg}`);
554
556
  }
555
557
  const data = await res.json();
556
- if (options.saveTo === "agent" || !options.saveTo) {
557
- const configDir = join(DEFAULT_CONFIG_DIR, "agents", agentId);
558
+ const isHuman = options.type === "human";
559
+ if ((options.saveTo === "agent" || !options.saveTo) && !isHuman) {
560
+ const configDir = join(DEFAULT_CONFIG_DIR, "agents", agentName);
558
561
  const configPath = `${configDir}/agent.yaml`;
559
562
  mkdirSync(configDir, {
560
563
  recursive: true,
561
564
  mode: 448
562
565
  });
563
- writeFileSync(configPath, `token: "${data.token}"\ntype: claude-code\n`, { mode: 384 });
566
+ writeFileSync(configPath, `token: "${data.token}"\nruntime: claude-code\n`, { mode: 384 });
564
567
  chmodSync(configDir, 448);
565
- } else if (options.saveTo) {
568
+ } else if (options.saveTo && options.saveTo !== "agent") {
566
569
  mkdirSync(dirname(options.saveTo), { recursive: true });
567
570
  writeFileSync(options.saveTo, data.token, { mode: 384 });
568
571
  }
@@ -578,16 +581,89 @@ function resolveAgentToken() {
578
581
  return token;
579
582
  }
580
583
  /**
584
+ * Ensure the persisted access token is fresh. Call before any admin API request
585
+ * when using persisted credentials. Returns the (possibly refreshed) access token.
586
+ */
587
+ async function ensureFreshAdminToken() {
588
+ const envToken = process.env.FIRST_TREE_HUB_ADMIN_TOKEN;
589
+ if (envToken) return envToken;
590
+ const creds = loadCredentials();
591
+ 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.");
592
+ if (!isTokenExpired(creds.accessToken)) return creds.accessToken;
593
+ const res = await fetch(`${creds.serverUrl}/api/v1/auth/refresh`, {
594
+ method: "POST",
595
+ headers: { "Content-Type": "application/json" },
596
+ body: JSON.stringify({ refreshToken: creds.refreshToken }),
597
+ signal: AbortSignal.timeout(1e4)
598
+ });
599
+ if (!res.ok) throw new Error("Access token expired and refresh failed.\n Run: first-tree-hub connect <server-url>");
600
+ const data = await res.json();
601
+ saveCredentials({
602
+ ...creds,
603
+ accessToken: data.accessToken
604
+ });
605
+ return data.accessToken;
606
+ }
607
+ /** Check if a JWT access token is expired (with 30s margin). */
608
+ function isTokenExpired(token) {
609
+ try {
610
+ const parts = token.split(".");
611
+ if (parts.length !== 3 || !parts[1]) return true;
612
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
613
+ if (!payload.exp) return false;
614
+ return payload.exp * 1e3 < Date.now() - 3e4;
615
+ } catch {
616
+ return true;
617
+ }
618
+ }
619
+ /** Persist credentials to disk. */
620
+ function saveCredentials(creds) {
621
+ mkdirSync(dirname(CREDENTIALS_PATH), {
622
+ recursive: true,
623
+ mode: 448
624
+ });
625
+ writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 384 });
626
+ }
627
+ /**
628
+ * Load persisted credentials saved by `connect` command.
629
+ */
630
+ function loadCredentials() {
631
+ try {
632
+ const data = JSON.parse(readFileSync(CREDENTIALS_PATH, "utf-8"));
633
+ if (data.accessToken && data.refreshToken && data.serverUrl) return data;
634
+ return null;
635
+ } catch {
636
+ return null;
637
+ }
638
+ }
639
+ /**
581
640
  * Check if an agent exists and is synced.
582
641
  */
583
- async function checkBootstrapStatus(serverUrl, agentId) {
642
+ async function checkBootstrapStatus(serverUrl, agentName) {
584
643
  const githubToken = getGitHubToken();
585
- const res = await fetch(`${serverUrl}/api/v1/bootstrap/${encodeURIComponent(agentId)}/status`, { headers: { "X-GitHub-Token": githubToken } });
644
+ const res = await fetch(`${serverUrl}/api/v1/bootstrap/${encodeURIComponent(agentName)}/status`, { headers: { "X-GitHub-Token": githubToken } });
586
645
  if (!res.ok) {
587
646
  const body = await res.json().catch(() => ({}));
588
647
  throw new Error(body.error ?? `HTTP ${res.status}`);
589
648
  }
590
649
  return await res.json();
591
650
  }
651
+ /**
652
+ * Write agent config (token + runtime) to disk.
653
+ * Used by `agent create`, `agent add`, bootstrap, and server-pushed provisioning.
654
+ */
655
+ function saveAgentConfig(agentName, token, runtime) {
656
+ const agentDir = join(DEFAULT_CONFIG_DIR, "agents", agentName);
657
+ mkdirSync(agentDir, {
658
+ recursive: true,
659
+ mode: 448
660
+ });
661
+ writeFileSync(join(agentDir, "agent.yaml"), `token: "${token}"\nruntime: ${runtime}\n`, { mode: 384 });
662
+ return agentDir;
663
+ }
664
+ /** Mask a token for display: show first 6 + last 2 chars. */
665
+ function maskToken(token) {
666
+ return token.length > 8 ? `${token.slice(0, 6)}***${token.slice(-2)}` : "***";
667
+ }
592
668
  //#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 };
669
+ export { resetConfigMeta as C, setConfigValue as E, resetConfig as S, serverConfigSchema as T, collectMissingPrompts as _, getGitHubToken as a, loadAgents as b, resolveAgentToken as c, saveCredentials as d, DEFAULT_CONFIG_DIR as f, clientConfigSchema as g, agentConfigSchema as h, ensureFreshAdminToken as i, resolveServerUrl as l, DEFAULT_HOME_DIR as m, bootstrap_exports as n, getGitHubUsername as o, DEFAULT_DATA_DIR as p, checkBootstrapStatus as r, maskToken as s, bootstrapToken as t, saveAgentConfig as u, getConfigValue as v, resolveConfigReadonly as w, readConfigFile as x, initConfig as y };