@agent-team-foundation/first-tree-hub 0.6.1 → 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,137 +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
- loadAgentTokenByName: () => loadAgentTokenByName,
491
478
  loadCredentials: () => loadCredentials,
492
- maskToken: () => maskToken,
493
- resolveAgentToken: () => resolveAgentToken,
479
+ resolveAccessToken: () => resolveAccessToken,
494
480
  resolveServerUrl: () => resolveServerUrl,
495
481
  saveAgentConfig: () => saveAgentConfig,
496
482
  saveCredentials: () => saveCredentials
497
483
  });
498
484
  const CREDENTIALS_PATH = join(DEFAULT_CONFIG_DIR, "credentials.json");
499
485
  /**
500
- * Get the current GitHub username from `gh auth status`.
501
- */
502
- function getGitHubUsername() {
503
- try {
504
- const output = execSync("gh api /user --jq .login", { encoding: "utf-8" }).trim();
505
- if (!output) throw new Error("Empty response");
506
- return output;
507
- } catch {
508
- throw new Error("Failed to get GitHub username. Ensure `gh` CLI is installed and authenticated:\n gh auth login");
509
- }
510
- }
511
- /**
512
- * Get the GitHub auth token from `gh auth token`.
513
- */
514
- function getGitHubToken() {
515
- try {
516
- const output = execSync("gh auth token", { encoding: "utf-8" }).trim();
517
- if (!output) throw new Error("Empty response");
518
- return output;
519
- } catch {
520
- throw new Error("Failed to get GitHub token. Ensure `gh` CLI is installed and authenticated:\n gh auth login");
521
- }
522
- }
523
- /**
524
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.
525
490
  */
526
491
  function resolveServerUrl(flagValue) {
527
492
  if (flagValue) return flagValue;
528
493
  if (process.env.FIRST_TREE_HUB_SERVER_URL) return process.env.FIRST_TREE_HUB_SERVER_URL;
529
- try {
530
- const config = getClientConfig();
531
- if (config.server?.url) return config.server.url;
532
- } catch {}
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>");
534
- }
535
- /**
536
- * Bootstrap a token for an agent using GitHub identity.
537
- */
538
- async function bootstrapToken(serverUrl, agentName, options = {}) {
539
- const githubToken = getGitHubToken();
540
- const body = { name: "bootstrap" };
541
- if (options.type) body.type = options.type;
542
- if (options.displayName) body.displayName = options.displayName;
543
- if (options.delegateMention) body.delegateMention = options.delegateMention;
544
- if (options.profile) body.profile = options.profile;
545
- if (options.metadata) body.metadata = options.metadata;
546
- const res = await fetch(`${serverUrl}/api/v1/bootstrap/${encodeURIComponent(agentName)}/token`, {
547
- method: "POST",
548
- headers: {
549
- "X-GitHub-Token": githubToken,
550
- "Content-Type": "application/json"
551
- },
552
- body: JSON.stringify(body)
553
- });
554
- if (!res.ok) {
555
- const msg = (await res.json().catch(() => ({}))).error ?? `HTTP ${res.status}`;
556
- throw new Error(`Bootstrap failed for "${agentName}": ${msg}`);
557
- }
558
- const data = await res.json();
559
- const isHuman = options.type === "human";
560
- if ((options.saveTo === "agent" || !options.saveTo) && !isHuman) {
561
- const configDir = join(DEFAULT_CONFIG_DIR, "agents", agentName);
562
- const configPath = `${configDir}/agent.yaml`;
563
- mkdirSync(configDir, {
564
- recursive: true,
565
- mode: 448
566
- });
567
- writeFileSync(configPath, `token: "${data.token}"\nruntime: claude-code\n`, { mode: 384 });
568
- chmodSync(configDir, 448);
569
- } else if (options.saveTo && options.saveTo !== "agent") {
570
- mkdirSync(dirname(options.saveTo), { recursive: true });
571
- writeFileSync(options.saveTo, data.token, { mode: 384 });
572
- }
573
- return data;
574
- }
575
- /**
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;
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;
588
501
  }
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>");
589
503
  }
590
504
  /**
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.
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.
595
511
  */
596
- function resolveAgentToken() {
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.");
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;
606
516
  }
607
517
  /**
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.
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.
610
521
  */
611
- async function ensureFreshAdminToken() {
612
- const envToken = process.env.FIRST_TREE_HUB_ADMIN_TOKEN;
613
- if (envToken) return envToken;
522
+ async function ensureFreshAccessToken() {
614
523
  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.");
524
+ if (!creds) throw new Error("No credentials found. Run `first-tree-hub connect <server-url>` to sign in.");
616
525
  if (!isTokenExpired(creds.accessToken)) return creds.accessToken;
617
526
  const res = await fetch(`${creds.serverUrl}/api/v1/auth/refresh`, {
618
527
  method: "POST",
@@ -620,7 +529,7 @@ async function ensureFreshAdminToken() {
620
529
  body: JSON.stringify({ refreshToken: creds.refreshToken }),
621
530
  signal: AbortSignal.timeout(1e4)
622
531
  });
623
- 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>`.");
624
533
  const data = await res.json();
625
534
  saveCredentials({
626
535
  ...creds,
@@ -628,7 +537,8 @@ async function ensureFreshAdminToken() {
628
537
  });
629
538
  return data.accessToken;
630
539
  }
631
- /** 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;
632
542
  function isTokenExpired(token) {
633
543
  try {
634
544
  const parts = token.split(".");
@@ -649,7 +559,7 @@ function saveCredentials(creds) {
649
559
  writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 384 });
650
560
  }
651
561
  /**
652
- * Load persisted credentials saved by `connect` command.
562
+ * Load persisted credentials saved by the `connect` command.
653
563
  */
654
564
  function loadCredentials() {
655
565
  try {
@@ -661,33 +571,16 @@ function loadCredentials() {
661
571
  }
662
572
  }
663
573
  /**
664
- * Check if an agent exists and is synced.
665
- */
666
- async function checkBootstrapStatus(serverUrl, agentName) {
667
- const githubToken = getGitHubToken();
668
- const res = await fetch(`${serverUrl}/api/v1/bootstrap/${encodeURIComponent(agentName)}/status`, { headers: { "X-GitHub-Token": githubToken } });
669
- if (!res.ok) {
670
- const body = await res.json().catch(() => ({}));
671
- throw new Error(body.error ?? `HTTP ${res.status}`);
672
- }
673
- return await res.json();
674
- }
675
- /**
676
- * Write agent config (token + runtime) to disk.
677
- * Used by `agent create`, `agent add`, bootstrap, and server-pushed provisioning.
574
+ * Write agent config (agentId + runtime) to disk.
678
575
  */
679
- function saveAgentConfig(agentName, token, runtime) {
576
+ function saveAgentConfig(agentName, agentId, runtime) {
680
577
  const agentDir = join(DEFAULT_CONFIG_DIR, "agents", agentName);
681
578
  mkdirSync(agentDir, {
682
579
  recursive: true,
683
580
  mode: 448
684
581
  });
685
- 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 });
686
583
  return agentDir;
687
584
  }
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
- }
692
585
  //#endregion
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 };
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 };