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

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",
@@ -61,6 +56,10 @@ const clientConfigSchema = defineConfig({
61
56
  default: "http://localhost:8000"
62
57
  }
63
58
  }) },
59
+ client: { id: field(z.string().regex(/^client_[a-f0-9]{8}$/), {
60
+ auto: "client-id",
61
+ env: "FIRST_TREE_HUB_CLIENT_ID"
62
+ }) },
64
63
  logLevel: field(z.enum([
65
64
  "debug",
66
65
  "info",
@@ -68,10 +67,6 @@ const clientConfigSchema = defineConfig({
68
67
  "error"
69
68
  ]).default("info"), { env: "FIRST_TREE_HUB_LOG_LEVEL" })
70
69
  });
71
- /** Typed accessor for client configuration singleton. */
72
- function getClientConfig() {
73
- return getConfig();
74
- }
75
70
  const DEFAULT_HOME_DIR = process.env.FIRST_TREE_HUB_HOME ?? join(homedir(), ".first-tree-hub");
76
71
  const DEFAULT_CONFIG_DIR = join(DEFAULT_HOME_DIR, "config");
77
72
  const DEFAULT_DATA_DIR = join(DEFAULT_HOME_DIR, "data");
@@ -121,6 +116,7 @@ function coerceEnvValue(value, schema) {
121
116
  return value;
122
117
  }
123
118
  function builtinAutoGenerate(strategy) {
119
+ if (strategy === "client-id") return `client_${randomBytes(4).toString("hex")}`;
124
120
  const match = /^random:(\w+):(\d+)$/.exec(strategy);
125
121
  if (!match) throw new Error(`Unknown auto-generation strategy: ${strategy}`);
126
122
  const encoding = match[1];
@@ -482,137 +478,55 @@ const serverConfigSchema = defineConfig({
482
478
  //#endregion
483
479
  //#region src/core/bootstrap.ts
484
480
  var bootstrap_exports = /* @__PURE__ */ __exportAll({
485
- bootstrapToken: () => bootstrapToken,
486
- checkBootstrapStatus: () => checkBootstrapStatus,
481
+ ensureFreshAccessToken: () => ensureFreshAccessToken,
487
482
  ensureFreshAdminToken: () => ensureFreshAdminToken,
488
- getGitHubToken: () => getGitHubToken,
489
- getGitHubUsername: () => getGitHubUsername,
490
- loadAgentTokenByName: () => loadAgentTokenByName,
491
483
  loadCredentials: () => loadCredentials,
492
- maskToken: () => maskToken,
493
- resolveAgentToken: () => resolveAgentToken,
484
+ resolveAccessToken: () => resolveAccessToken,
494
485
  resolveServerUrl: () => resolveServerUrl,
495
486
  saveAgentConfig: () => saveAgentConfig,
496
487
  saveCredentials: () => saveCredentials
497
488
  });
498
489
  const CREDENTIALS_PATH = join(DEFAULT_CONFIG_DIR, "credentials.json");
499
490
  /**
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
491
  * Resolve Hub server URL from flag, env, or config.
492
+ *
493
+ * Uses resolveConfigReadonly (not the singleton getClientConfig) so CLI entry
494
+ * points don't have to remember to call initConfig() first.
525
495
  */
526
496
  function resolveServerUrl(flagValue) {
527
497
  if (flagValue) return flagValue;
528
498
  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;
499
+ const server = resolveConfigReadonly({
500
+ schema: clientConfigSchema,
501
+ role: "client"
502
+ }).server;
503
+ if (server !== null && typeof server === "object") {
504
+ const url = Reflect.get(server, "url");
505
+ if (typeof url === "string" && url.length > 0) return url;
588
506
  }
507
+ 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
508
  }
590
509
  /**
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.
510
+ * Resolve the current member access JWT from persisted credentials.
511
+ *
512
+ * Unified-user-token milestone: the CLI has a single credential store and a
513
+ * single onboarding path (`first-tree-hub client connect`). The legacy
514
+ * `FIRST_TREE_HUB_TOKEN` env var is no longer read — callers get a clear
515
+ * error pointing at `client connect` instead.
595
516
  */
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.");
517
+ function resolveAccessToken() {
518
+ const creds = loadCredentials();
519
+ if (!creds) throw new Error("No credentials found. Run `first-tree-hub client connect <server-url>` to sign in.");
520
+ return creds.accessToken;
606
521
  }
607
522
  /**
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.
523
+ * Ensure the persisted access token is fresh. Call before any API request
524
+ * when using persisted credentials. Returns the (possibly refreshed) access
525
+ * token. Service-user API keys are out of scope for this milestone.
610
526
  */
611
- async function ensureFreshAdminToken() {
612
- const envToken = process.env.FIRST_TREE_HUB_ADMIN_TOKEN;
613
- if (envToken) return envToken;
527
+ async function ensureFreshAccessToken() {
614
528
  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.");
529
+ if (!creds) throw new Error("No credentials found. Run `first-tree-hub client connect <server-url>` to sign in.");
616
530
  if (!isTokenExpired(creds.accessToken)) return creds.accessToken;
617
531
  const res = await fetch(`${creds.serverUrl}/api/v1/auth/refresh`, {
618
532
  method: "POST",
@@ -620,7 +534,7 @@ async function ensureFreshAdminToken() {
620
534
  body: JSON.stringify({ refreshToken: creds.refreshToken }),
621
535
  signal: AbortSignal.timeout(1e4)
622
536
  });
623
- if (!res.ok) throw new Error("Access token expired and refresh failed.\n Run: first-tree-hub connect <server-url>");
537
+ if (!res.ok) throw new Error("Access token expired and refresh failed. Run `first-tree-hub client connect <server-url>`.");
624
538
  const data = await res.json();
625
539
  saveCredentials({
626
540
  ...creds,
@@ -628,7 +542,8 @@ async function ensureFreshAdminToken() {
628
542
  });
629
543
  return data.accessToken;
630
544
  }
631
- /** Check if a JWT access token is expired (with 30s margin). */
545
+ /** Back-compat alias retained so existing call sites keep compiling. */
546
+ const ensureFreshAdminToken = ensureFreshAccessToken;
632
547
  function isTokenExpired(token) {
633
548
  try {
634
549
  const parts = token.split(".");
@@ -649,7 +564,7 @@ function saveCredentials(creds) {
649
564
  writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 384 });
650
565
  }
651
566
  /**
652
- * Load persisted credentials saved by `connect` command.
567
+ * Load persisted credentials saved by the `connect` command.
653
568
  */
654
569
  function loadCredentials() {
655
570
  try {
@@ -661,33 +576,16 @@ function loadCredentials() {
661
576
  }
662
577
  }
663
578
  /**
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.
579
+ * Write agent config (agentId + runtime) to disk.
678
580
  */
679
- function saveAgentConfig(agentName, token, runtime) {
581
+ function saveAgentConfig(agentName, agentId, runtime) {
680
582
  const agentDir = join(DEFAULT_CONFIG_DIR, "agents", agentName);
681
583
  mkdirSync(agentDir, {
682
584
  recursive: true,
683
585
  mode: 448
684
586
  });
685
- writeFileSync(join(agentDir, "agent.yaml"), `token: "${token}"\nruntime: ${runtime}\n`, { mode: 384 });
587
+ writeFileSync(join(agentDir, "agent.yaml"), `agentId: "${agentId}"\nruntime: ${runtime}\n`, { mode: 384 });
686
588
  return agentDir;
687
589
  }
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
590
  //#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 };
591
+ 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 };