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

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,10 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-twds-ZHy.mjs";
2
- import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { z } from "zod";
5
5
  import { parse, stringify } from "yaml";
6
6
  import { randomBytes } from "node:crypto";
7
7
  import { homedir } from "node:os";
8
- import { execSync } from "node:child_process";
9
8
  //#region ../shared/dist/config/index.mjs
10
9
  /** Declare a config field with a Zod schema and optional metadata. */
11
10
  function field(schema, options) {
@@ -27,8 +26,17 @@ function optional(shape) {
27
26
  function defineConfig(shape) {
28
27
  return shape;
29
28
  }
29
+ /**
30
+ * Agent config layout on disk: `~/.first-tree-hub/config/agents/<name>/agent.yaml`.
31
+ *
32
+ * After the unified-user-token milestone the local config no longer stores an
33
+ * agent bearer; authentication comes from the user's member JWT in
34
+ * `credentials.json`. The config just pins the agent UUID and its runtime so
35
+ * the runtime knows which agent to act as (via `X-Agent-Id`) and which
36
+ * handler to instantiate.
37
+ */
30
38
  const agentConfigSchema = defineConfig({
31
- token: field(z.string(), { secret: true }),
39
+ agentId: field(z.string().min(1)),
32
40
  runtime: field(z.string().default("claude-code")),
33
41
  concurrency: field(z.number().int().positive().default(5)),
34
42
  session: {
@@ -36,23 +44,10 @@ const agentConfigSchema = defineConfig({
36
44
  max_sessions: field(z.number().int().positive().default(10))
37
45
  }
38
46
  });
39
- let _config;
40
47
  /** Store the resolved config as a singleton. Called by initConfig(). */
41
- function setConfig(config) {
42
- _config = config;
43
- }
44
- /**
45
- * Get the resolved config singleton.
46
- * Must be called after initConfig().
47
- */
48
- function getConfig() {
49
- if (_config === void 0) throw new Error("Config not initialized. Call initConfig() first.");
50
- return _config;
51
- }
48
+ function setConfig(config) {}
52
49
  /** Reset the config singleton. For testing only. */
53
- function resetConfig() {
54
- _config = void 0;
55
- }
50
+ function resetConfig() {}
56
51
  const clientConfigSchema = defineConfig({
57
52
  server: { url: field(z.string(), {
58
53
  env: "FIRST_TREE_HUB_SERVER_URL",
@@ -68,10 +63,6 @@ const clientConfigSchema = defineConfig({
68
63
  "error"
69
64
  ]).default("info"), { env: "FIRST_TREE_HUB_LOG_LEVEL" })
70
65
  });
71
- /** Typed accessor for client configuration singleton. */
72
- function getClientConfig() {
73
- return getConfig();
74
- }
75
66
  const DEFAULT_HOME_DIR = process.env.FIRST_TREE_HUB_HOME ?? join(homedir(), ".first-tree-hub");
76
67
  const DEFAULT_CONFIG_DIR = join(DEFAULT_HOME_DIR, "config");
77
68
  const DEFAULT_DATA_DIR = join(DEFAULT_HOME_DIR, "data");
@@ -482,113 +473,55 @@ const serverConfigSchema = defineConfig({
482
473
  //#endregion
483
474
  //#region src/core/bootstrap.ts
484
475
  var bootstrap_exports = /* @__PURE__ */ __exportAll({
485
- bootstrapToken: () => bootstrapToken,
486
- checkBootstrapStatus: () => checkBootstrapStatus,
476
+ ensureFreshAccessToken: () => ensureFreshAccessToken,
487
477
  ensureFreshAdminToken: () => ensureFreshAdminToken,
488
- getGitHubToken: () => getGitHubToken,
489
- getGitHubUsername: () => getGitHubUsername,
490
478
  loadCredentials: () => loadCredentials,
491
- maskToken: () => maskToken,
492
- resolveAgentToken: () => resolveAgentToken,
479
+ resolveAccessToken: () => resolveAccessToken,
493
480
  resolveServerUrl: () => resolveServerUrl,
494
481
  saveAgentConfig: () => saveAgentConfig,
495
482
  saveCredentials: () => saveCredentials
496
483
  });
497
484
  const CREDENTIALS_PATH = join(DEFAULT_CONFIG_DIR, "credentials.json");
498
485
  /**
499
- * Get the current GitHub username from `gh auth status`.
500
- */
501
- function getGitHubUsername() {
502
- try {
503
- const output = execSync("gh api /user --jq .login", { encoding: "utf-8" }).trim();
504
- if (!output) throw new Error("Empty response");
505
- return output;
506
- } catch {
507
- throw new Error("Failed to get GitHub username. Ensure `gh` CLI is installed and authenticated:\n gh auth login");
508
- }
509
- }
510
- /**
511
- * Get the GitHub auth token from `gh auth token`.
512
- */
513
- function getGitHubToken() {
514
- try {
515
- const output = execSync("gh auth token", { encoding: "utf-8" }).trim();
516
- if (!output) throw new Error("Empty response");
517
- return output;
518
- } catch {
519
- throw new Error("Failed to get GitHub token. Ensure `gh` CLI is installed and authenticated:\n gh auth login");
520
- }
521
- }
522
- /**
523
486
  * Resolve Hub server URL from flag, env, or config.
487
+ *
488
+ * Uses resolveConfigReadonly (not the singleton getClientConfig) so CLI entry
489
+ * points don't have to remember to call initConfig() first.
524
490
  */
525
491
  function resolveServerUrl(flagValue) {
526
492
  if (flagValue) return flagValue;
527
- if (process.env.FIRST_TREE_HUB_SERVER) return process.env.FIRST_TREE_HUB_SERVER;
528
- try {
529
- const config = getClientConfig();
530
- if (config.server?.url) return config.server.url;
531
- } catch {}
532
- 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
- }
534
- /**
535
- * Bootstrap a token for an agent using GitHub identity.
536
- */
537
- async function bootstrapToken(serverUrl, agentName, options = {}) {
538
- const githubToken = getGitHubToken();
539
- const body = { name: "bootstrap" };
540
- if (options.type) body.type = options.type;
541
- if (options.displayName) body.displayName = options.displayName;
542
- if (options.delegateMention) body.delegateMention = options.delegateMention;
543
- if (options.profile) body.profile = options.profile;
544
- if (options.metadata) body.metadata = options.metadata;
545
- const res = await fetch(`${serverUrl}/api/v1/bootstrap/${encodeURIComponent(agentName)}/token`, {
546
- method: "POST",
547
- headers: {
548
- "X-GitHub-Token": githubToken,
549
- "Content-Type": "application/json"
550
- },
551
- body: JSON.stringify(body)
552
- });
553
- if (!res.ok) {
554
- const msg = (await res.json().catch(() => ({}))).error ?? `HTTP ${res.status}`;
555
- throw new Error(`Bootstrap failed for "${agentName}": ${msg}`);
493
+ if (process.env.FIRST_TREE_HUB_SERVER_URL) return process.env.FIRST_TREE_HUB_SERVER_URL;
494
+ const server = resolveConfigReadonly({
495
+ schema: clientConfigSchema,
496
+ role: "client"
497
+ }).server;
498
+ if (server !== null && typeof server === "object") {
499
+ const url = Reflect.get(server, "url");
500
+ if (typeof url === "string" && url.length > 0) return url;
556
501
  }
557
- const data = await res.json();
558
- const isHuman = options.type === "human";
559
- if ((options.saveTo === "agent" || !options.saveTo) && !isHuman) {
560
- const configDir = join(DEFAULT_CONFIG_DIR, "agents", agentName);
561
- const configPath = `${configDir}/agent.yaml`;
562
- mkdirSync(configDir, {
563
- recursive: true,
564
- mode: 448
565
- });
566
- writeFileSync(configPath, `token: "${data.token}"\nruntime: claude-code\n`, { mode: 384 });
567
- chmodSync(configDir, 448);
568
- } else if (options.saveTo && options.saveTo !== "agent") {
569
- mkdirSync(dirname(options.saveTo), { recursive: true });
570
- writeFileSync(options.saveTo, data.token, { mode: 384 });
571
- }
572
- return data;
502
+ 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>");
573
503
  }
574
504
  /**
575
- * Resolve agent token from FIRST_TREE_HUB_TOKEN env var.
576
- * Throws if not set.
505
+ * Resolve the current member access JWT from persisted credentials.
506
+ *
507
+ * Unified-user-token milestone: the CLI has a single credential store and a
508
+ * single onboarding path (`first-tree-hub connect`). The legacy
509
+ * `FIRST_TREE_HUB_TOKEN` env var is no longer read — callers get a clear
510
+ * error pointing at `connect` instead.
577
511
  */
578
- function resolveAgentToken() {
579
- const token = process.env.FIRST_TREE_HUB_TOKEN;
580
- if (!token) throw new Error("FIRST_TREE_HUB_TOKEN environment variable is required.");
581
- return token;
512
+ function resolveAccessToken() {
513
+ const creds = loadCredentials();
514
+ if (!creds) throw new Error("No credentials found. Run `first-tree-hub connect <server-url>` to sign in.");
515
+ return creds.accessToken;
582
516
  }
583
517
  /**
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.
518
+ * Ensure the persisted access token is fresh. Call before any API request
519
+ * when using persisted credentials. Returns the (possibly refreshed) access
520
+ * token. Service-user API keys are out of scope for this milestone.
586
521
  */
587
- async function ensureFreshAdminToken() {
588
- const envToken = process.env.FIRST_TREE_HUB_ADMIN_TOKEN;
589
- if (envToken) return envToken;
522
+ async function ensureFreshAccessToken() {
590
523
  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.");
524
+ if (!creds) throw new Error("No credentials found. Run `first-tree-hub connect <server-url>` to sign in.");
592
525
  if (!isTokenExpired(creds.accessToken)) return creds.accessToken;
593
526
  const res = await fetch(`${creds.serverUrl}/api/v1/auth/refresh`, {
594
527
  method: "POST",
@@ -596,7 +529,7 @@ async function ensureFreshAdminToken() {
596
529
  body: JSON.stringify({ refreshToken: creds.refreshToken }),
597
530
  signal: AbortSignal.timeout(1e4)
598
531
  });
599
- if (!res.ok) throw new Error("Access token expired and refresh failed.\n Run: first-tree-hub connect <server-url>");
532
+ if (!res.ok) throw new Error("Access token expired and refresh failed. Run `first-tree-hub connect <server-url>`.");
600
533
  const data = await res.json();
601
534
  saveCredentials({
602
535
  ...creds,
@@ -604,7 +537,8 @@ async function ensureFreshAdminToken() {
604
537
  });
605
538
  return data.accessToken;
606
539
  }
607
- /** Check if a JWT access token is expired (with 30s margin). */
540
+ /** Back-compat alias retained so existing call sites keep compiling. */
541
+ const ensureFreshAdminToken = ensureFreshAccessToken;
608
542
  function isTokenExpired(token) {
609
543
  try {
610
544
  const parts = token.split(".");
@@ -625,7 +559,7 @@ function saveCredentials(creds) {
625
559
  writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 384 });
626
560
  }
627
561
  /**
628
- * Load persisted credentials saved by `connect` command.
562
+ * Load persisted credentials saved by the `connect` command.
629
563
  */
630
564
  function loadCredentials() {
631
565
  try {
@@ -637,33 +571,16 @@ function loadCredentials() {
637
571
  }
638
572
  }
639
573
  /**
640
- * Check if an agent exists and is synced.
574
+ * Write agent config (agentId + runtime) to disk.
641
575
  */
642
- async function checkBootstrapStatus(serverUrl, agentName) {
643
- const githubToken = getGitHubToken();
644
- const res = await fetch(`${serverUrl}/api/v1/bootstrap/${encodeURIComponent(agentName)}/status`, { headers: { "X-GitHub-Token": githubToken } });
645
- if (!res.ok) {
646
- const body = await res.json().catch(() => ({}));
647
- throw new Error(body.error ?? `HTTP ${res.status}`);
648
- }
649
- return await res.json();
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) {
576
+ function saveAgentConfig(agentName, agentId, runtime) {
656
577
  const agentDir = join(DEFAULT_CONFIG_DIR, "agents", agentName);
657
578
  mkdirSync(agentDir, {
658
579
  recursive: true,
659
580
  mode: 448
660
581
  });
661
- writeFileSync(join(agentDir, "agent.yaml"), `token: "${token}"\nruntime: ${runtime}\n`, { mode: 384 });
582
+ writeFileSync(join(agentDir, "agent.yaml"), `agentId: "${agentId}"\nruntime: ${runtime}\n`, { mode: 384 });
662
583
  return agentDir;
663
584
  }
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
- }
668
585
  //#endregion
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 };
586
+ export { setConfigValue as C, serverConfigSchema as S, loadAgents as _, resolveAccessToken as a, resetConfigMeta as b, saveCredentials as c, DEFAULT_HOME_DIR as d, agentConfigSchema as f, initConfig as g, getConfigValue as h, loadCredentials as i, DEFAULT_CONFIG_DIR as l, collectMissingPrompts as m, ensureFreshAccessToken as n, resolveServerUrl as o, clientConfigSchema as p, ensureFreshAdminToken as r, saveAgentConfig as s, bootstrap_exports as t, DEFAULT_DATA_DIR as u, readConfigFile as v, resolveConfigReadonly as x, resetConfig as y };