@agent-team-foundation/first-tree-hub 0.3.0 → 0.3.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.
@@ -0,0 +1,583 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-twds-ZHy.mjs";
2
+ import { z } from "zod";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { parse, stringify } from "yaml";
6
+ import { randomBytes } from "node:crypto";
7
+ import { homedir } from "node:os";
8
+ import { execSync } from "node:child_process";
9
+ //#region ../shared/dist/config/index.mjs
10
+ /** Declare a config field with a Zod schema and optional metadata. */
11
+ function field(schema, options) {
12
+ return {
13
+ _tag: "field",
14
+ _type: void 0,
15
+ schema,
16
+ options: options ?? {}
17
+ };
18
+ }
19
+ /** Mark a config group as optional — present only when at least one field has an explicit value. */
20
+ function optional(shape) {
21
+ return {
22
+ _tag: "optional",
23
+ shape
24
+ };
25
+ }
26
+ /** Define a config shape. Identity function used for type inference. */
27
+ function defineConfig(shape) {
28
+ return shape;
29
+ }
30
+ const agentConfigSchema = defineConfig({
31
+ token: field(z.string(), { secret: true }),
32
+ type: field(z.string().default("claude-code")),
33
+ concurrency: field(z.number().int().positive().default(5)),
34
+ session: {
35
+ idle_timeout: field(z.number().int().positive().default(300)),
36
+ max_sessions: field(z.number().int().positive().default(10))
37
+ }
38
+ });
39
+ let _config;
40
+ /** 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
+ }
52
+ /** Reset the config singleton. For testing only. */
53
+ function resetConfig() {
54
+ _config = void 0;
55
+ }
56
+ const clientConfigSchema = defineConfig({
57
+ server: { url: field(z.string(), {
58
+ env: "FIRST_TREE_HUB_SERVER_URL",
59
+ prompt: {
60
+ message: "Server URL:",
61
+ default: "http://localhost:8000"
62
+ }
63
+ }) },
64
+ logLevel: field(z.enum([
65
+ "debug",
66
+ "info",
67
+ "warn",
68
+ "error"
69
+ ]).default("info"), { env: "FIRST_TREE_HUB_LOG_LEVEL" })
70
+ });
71
+ /** Typed accessor for client configuration singleton. */
72
+ function getClientConfig() {
73
+ return getConfig();
74
+ }
75
+ const DEFAULT_HOME_DIR = join(homedir(), ".first-tree-hub");
76
+ const DEFAULT_CONFIG_DIR = join(DEFAULT_HOME_DIR, "config");
77
+ const DEFAULT_DATA_DIR = join(DEFAULT_HOME_DIR, "data");
78
+ function isFieldDef(value) {
79
+ return typeof value === "object" && value !== null && "_tag" in value && value._tag === "field";
80
+ }
81
+ function isOptionalGroup(value) {
82
+ return typeof value === "object" && value !== null && "_tag" in value && value._tag === "optional";
83
+ }
84
+ function getByPath(obj, path) {
85
+ let current = obj;
86
+ for (const key of path) {
87
+ if (current === null || current === void 0 || typeof current !== "object") return;
88
+ current = current[key];
89
+ }
90
+ return current;
91
+ }
92
+ function setByPath(obj, path, value) {
93
+ let current = obj;
94
+ for (let i = 0; i < path.length - 1; i++) {
95
+ const key = path[i];
96
+ if (key === void 0) continue;
97
+ if (!(key in current) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
98
+ current = current[key];
99
+ }
100
+ const lastKey = path.at(-1);
101
+ if (lastKey !== void 0) current[lastKey] = value;
102
+ }
103
+ /** Unwrap ZodDefault / ZodOptional to get the inner type for coercion. */
104
+ function unwrapZodType(schema) {
105
+ if (schema instanceof z.ZodDefault) return unwrapZodType(schema._def.innerType);
106
+ if (schema instanceof z.ZodOptional) return unwrapZodType(schema._def.innerType);
107
+ return schema;
108
+ }
109
+ /** Coerce a string env var to the JS type expected by the Zod schema. */
110
+ function coerceEnvValue(value, schema) {
111
+ const inner = unwrapZodType(schema);
112
+ if (inner instanceof z.ZodNumber) {
113
+ const num = Number(value);
114
+ return Number.isNaN(num) ? value : num;
115
+ }
116
+ if (inner instanceof z.ZodBoolean) {
117
+ if (value === "true" || value === "1") return true;
118
+ if (value === "false" || value === "0") return false;
119
+ return value;
120
+ }
121
+ return value;
122
+ }
123
+ function builtinAutoGenerate(strategy) {
124
+ const match = /^random:(\w+):(\d+)$/.exec(strategy);
125
+ if (!match) throw new Error(`Unknown auto-generation strategy: ${strategy}`);
126
+ const encoding = match[1];
127
+ const bytes = Number(match[2]);
128
+ if (!encoding) throw new Error(`Invalid auto-generation strategy: ${strategy}`);
129
+ if (encoding === "base64url") return randomBytes(bytes).toString("base64url");
130
+ if (encoding === "hex") return randomBytes(bytes).toString("hex");
131
+ throw new Error(`Unknown random encoding: ${encoding}`);
132
+ }
133
+ function ensureDir(dir) {
134
+ if (!existsSync(dir)) mkdirSync(dir, {
135
+ recursive: true,
136
+ mode: 448
137
+ });
138
+ }
139
+ function deepMerge(target, source) {
140
+ const result = { ...target };
141
+ for (const [key, value] of Object.entries(source)) if (typeof value === "object" && value !== null && !Array.isArray(value) && typeof result[key] === "object" && result[key] !== null && !Array.isArray(result[key])) result[key] = deepMerge(result[key], value);
142
+ else result[key] = value;
143
+ return result;
144
+ }
145
+ function deepFreeze(obj) {
146
+ if (typeof obj !== "object" || obj === null) return obj;
147
+ Object.freeze(obj);
148
+ for (const value of Object.values(obj)) deepFreeze(value);
149
+ return obj;
150
+ }
151
+ function collectFields(shape, path = [], optionalGroupPath = null) {
152
+ const fields = [];
153
+ for (const [key, value] of Object.entries(shape)) {
154
+ const currentPath = [...path, key];
155
+ if (isFieldDef(value)) fields.push({
156
+ path: currentPath,
157
+ fieldDef: value,
158
+ optionalGroupPath
159
+ });
160
+ else if (isOptionalGroup(value)) fields.push(...collectFields(value.shape, currentPath, currentPath));
161
+ else if (typeof value === "object" && value !== null) fields.push(...collectFields(value, currentPath, optionalGroupPath));
162
+ }
163
+ return fields;
164
+ }
165
+ /** Build a Zod object schema from the config shape for validation. */
166
+ function buildZodSchema(shape) {
167
+ const zodShape = {};
168
+ for (const [key, value] of Object.entries(shape)) if (isFieldDef(value)) zodShape[key] = value.schema;
169
+ else if (isOptionalGroup(value)) zodShape[key] = buildZodSchema(value.shape).optional();
170
+ else if (typeof value === "object" && value !== null) zodShape[key] = buildZodSchema(value).default({});
171
+ return z.object(zodShape);
172
+ }
173
+ function resetConfigMeta() {}
174
+ const CONFIG_HEADER = "# Generated by first-tree-hub. Edit as needed.\n# https://github.com/agent-team-foundation/first-tree-hub\n\n";
175
+ /**
176
+ * Initialize config from the priority chain:
177
+ * CLI args > env vars > YAML file > auto-generated > Zod defaults
178
+ *
179
+ * Auto-generated values are written back to the YAML file.
180
+ * Result is frozen and stored as a singleton accessible via getConfig().
181
+ */
182
+ async function initConfig(options) {
183
+ const { schema, role, cliArgs = {}, autoGenerators = {} } = options;
184
+ const configPath = join(options.configDir ?? DEFAULT_CONFIG_DIR, `${role}.yaml`);
185
+ let fileValues = {};
186
+ if (existsSync(configPath)) {
187
+ const raw = parse(readFileSync(configPath, "utf-8"));
188
+ if (typeof raw === "object" && raw !== null) fileValues = raw;
189
+ }
190
+ const fields = collectFields(schema);
191
+ const resolved = {};
192
+ const meta = /* @__PURE__ */ new Map();
193
+ const autoGenerated = {};
194
+ const activeOptionalGroups = /* @__PURE__ */ new Set();
195
+ for (const { path, fieldDef, optionalGroupPath } of fields) {
196
+ if (!optionalGroupPath) continue;
197
+ const groupKey = optionalGroupPath.join(".");
198
+ if (activeOptionalGroups.has(groupKey)) continue;
199
+ if (getByPath(cliArgs, path) !== void 0) {
200
+ activeOptionalGroups.add(groupKey);
201
+ continue;
202
+ }
203
+ if (fieldDef.options.env) {
204
+ const envValue = process.env[fieldDef.options.env];
205
+ if (envValue !== void 0 && envValue !== "") {
206
+ activeOptionalGroups.add(groupKey);
207
+ continue;
208
+ }
209
+ }
210
+ if (getByPath(fileValues, path) !== void 0) activeOptionalGroups.add(groupKey);
211
+ }
212
+ for (const { path, fieldDef, optionalGroupPath } of fields) {
213
+ const dotPath = path.join(".");
214
+ if (optionalGroupPath && !activeOptionalGroups.has(optionalGroupPath.join("."))) continue;
215
+ const cliValue = getByPath(cliArgs, path);
216
+ if (cliValue !== void 0) {
217
+ setByPath(resolved, path, cliValue);
218
+ meta.set(dotPath, {
219
+ value: cliValue,
220
+ source: "cli",
221
+ secret: fieldDef.options.secret ?? false
222
+ });
223
+ continue;
224
+ }
225
+ if (fieldDef.options.env) {
226
+ const envValue = process.env[fieldDef.options.env];
227
+ if (envValue !== void 0 && envValue !== "") {
228
+ const coerced = coerceEnvValue(envValue, fieldDef.schema);
229
+ setByPath(resolved, path, coerced);
230
+ meta.set(dotPath, {
231
+ value: coerced,
232
+ source: "env",
233
+ secret: fieldDef.options.secret ?? false
234
+ });
235
+ continue;
236
+ }
237
+ }
238
+ const fileValue = getByPath(fileValues, path);
239
+ if (fileValue !== void 0) {
240
+ setByPath(resolved, path, fileValue);
241
+ meta.set(dotPath, {
242
+ value: fileValue,
243
+ source: "file",
244
+ secret: fieldDef.options.secret ?? false
245
+ });
246
+ continue;
247
+ }
248
+ if (fieldDef.options.auto) {
249
+ const strategy = fieldDef.options.auto;
250
+ const customGen = autoGenerators[strategy];
251
+ let generated;
252
+ if (customGen) generated = await customGen();
253
+ else generated = builtinAutoGenerate(strategy);
254
+ setByPath(resolved, path, generated);
255
+ setByPath(autoGenerated, path, generated);
256
+ meta.set(dotPath, {
257
+ value: generated,
258
+ source: "auto",
259
+ secret: fieldDef.options.secret ?? false
260
+ });
261
+ continue;
262
+ }
263
+ meta.set(dotPath, {
264
+ value: void 0,
265
+ source: "default",
266
+ secret: fieldDef.options.secret ?? false
267
+ });
268
+ }
269
+ const result = buildZodSchema(schema).safeParse(resolved);
270
+ if (!result.success) {
271
+ const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
272
+ throw new Error(`Configuration validation failed:\n${issues}`);
273
+ }
274
+ const config = result.data;
275
+ for (const { path, fieldDef } of fields) {
276
+ const dotPath = path.join(".");
277
+ if (meta.get(dotPath)?.value === void 0) {
278
+ const val = getByPath(config, path);
279
+ if (val !== void 0) meta.set(dotPath, {
280
+ value: val,
281
+ source: "default",
282
+ secret: fieldDef.options.secret ?? false
283
+ });
284
+ }
285
+ }
286
+ if (Object.keys(autoGenerated).length > 0) {
287
+ const merged = deepMerge(fileValues, autoGenerated);
288
+ ensureDir(dirname(configPath));
289
+ writeFileSync(configPath, CONFIG_HEADER + stringify(merged), { mode: 384 });
290
+ }
291
+ const frozen = deepFreeze(config);
292
+ setConfig(frozen);
293
+ return frozen;
294
+ }
295
+ /** Set a value in a YAML config file by dot-path. */
296
+ function setConfigValue(configPath, dotPath, value) {
297
+ let fileValues = {};
298
+ if (existsSync(configPath)) {
299
+ const raw = parse(readFileSync(configPath, "utf-8"));
300
+ if (typeof raw === "object" && raw !== null) fileValues = raw;
301
+ }
302
+ setByPath(fileValues, dotPath.split("."), value);
303
+ ensureDir(dirname(configPath));
304
+ writeFileSync(configPath, CONFIG_HEADER + stringify(fileValues), { mode: 384 });
305
+ }
306
+ /** Get a value from a YAML config file by dot-path. */
307
+ function getConfigValue(configPath, dotPath) {
308
+ if (!existsSync(configPath)) return void 0;
309
+ const raw = parse(readFileSync(configPath, "utf-8"));
310
+ if (typeof raw !== "object" || raw === null) return void 0;
311
+ return getByPath(raw, dotPath.split("."));
312
+ }
313
+ /** Read all values from a YAML config file. */
314
+ function readConfigFile(configPath) {
315
+ if (!existsSync(configPath)) return {};
316
+ const raw = parse(readFileSync(configPath, "utf-8"));
317
+ if (typeof raw !== "object" || raw === null) return {};
318
+ return raw;
319
+ }
320
+ /**
321
+ * Scan a config schema and return fields that:
322
+ * 1. Have a `prompt` definition
323
+ * 2. Don't have a value from CLI args, env vars, or the config file
324
+ * 3. Don't have an `auto` strategy (auto-gen fields don't need prompting)
325
+ *
326
+ * Used by CLI to show interactive prompts before calling initConfig().
327
+ */
328
+ function collectMissingPrompts(options) {
329
+ const { schema, role, cliArgs = {} } = options;
330
+ const configPath = join(options.configDir ?? DEFAULT_CONFIG_DIR, `${role}.yaml`);
331
+ let fileValues = {};
332
+ if (existsSync(configPath)) {
333
+ const raw = parse(readFileSync(configPath, "utf-8"));
334
+ if (typeof raw === "object" && raw !== null) fileValues = raw;
335
+ }
336
+ const fields = collectFields(schema);
337
+ const missing = [];
338
+ for (const { path, fieldDef, optionalGroupPath } of fields) {
339
+ if (optionalGroupPath) continue;
340
+ if (!fieldDef.options.prompt) continue;
341
+ if (getByPath(cliArgs, path) !== void 0) continue;
342
+ if (fieldDef.options.env) {
343
+ const envValue = process.env[fieldDef.options.env];
344
+ if (envValue !== void 0 && envValue !== "") continue;
345
+ }
346
+ if (getByPath(fileValues, path) !== void 0) continue;
347
+ missing.push({
348
+ dotPath: path.join("."),
349
+ prompt: fieldDef.options.prompt
350
+ });
351
+ }
352
+ return missing;
353
+ }
354
+ /**
355
+ * Resolve config values through the same priority chain as initConfig()
356
+ * (env vars > YAML file > Zod defaults), but **without side effects**:
357
+ * - No auto-generation
358
+ * - No file writes
359
+ * - No singleton mutation
360
+ * - Partial results (unresolvable fields are omitted, no validation error)
361
+ *
362
+ * Returns the best-effort resolved config as a plain object.
363
+ */
364
+ function resolveConfigReadonly(options) {
365
+ const { schema, role } = options;
366
+ const configPath = join(options.configDir ?? DEFAULT_CONFIG_DIR, `${role}.yaml`);
367
+ let fileValues = {};
368
+ if (existsSync(configPath)) {
369
+ const raw = parse(readFileSync(configPath, "utf-8"));
370
+ if (typeof raw === "object" && raw !== null) fileValues = raw;
371
+ }
372
+ const fields = collectFields(schema);
373
+ const resolved = {};
374
+ for (const { path, fieldDef } of fields) {
375
+ if (fieldDef.options.env) {
376
+ const envValue = process.env[fieldDef.options.env];
377
+ if (envValue !== void 0 && envValue !== "") {
378
+ setByPath(resolved, path, coerceEnvValue(envValue, fieldDef.schema));
379
+ continue;
380
+ }
381
+ }
382
+ const fileValue = getByPath(fileValues, path);
383
+ if (fileValue !== void 0) {
384
+ setByPath(resolved, path, fileValue);
385
+ continue;
386
+ }
387
+ const defaultResult = fieldDef.schema.safeParse(void 0);
388
+ if (defaultResult.success && defaultResult.data !== void 0) setByPath(resolved, path, defaultResult.data);
389
+ }
390
+ return resolved;
391
+ }
392
+ /**
393
+ * Scan an agents directory and load each agent's config.
394
+ *
395
+ * Expected structure:
396
+ * {agentsDir}/
397
+ * code-reviewer/agent.yaml
398
+ * scheduler/agent.yaml
399
+ *
400
+ * Returns a Map keyed by directory name (agent name).
401
+ */
402
+ function loadAgents(options) {
403
+ const { schema, agentsDir } = options;
404
+ const result = /* @__PURE__ */ new Map();
405
+ if (!existsSync(agentsDir)) return result;
406
+ const zodSchema = buildZodSchema(schema);
407
+ for (const entry of readdirSync(agentsDir)) {
408
+ const agentDir = join(agentsDir, entry);
409
+ if (!statSync(agentDir).isDirectory()) continue;
410
+ const configPath = join(agentDir, "agent.yaml");
411
+ if (!existsSync(configPath)) continue;
412
+ const raw = parse(readFileSync(configPath, "utf-8"));
413
+ const parsed = zodSchema.parse(raw);
414
+ result.set(entry, parsed);
415
+ }
416
+ return result;
417
+ }
418
+ const serverConfigSchema = defineConfig({
419
+ database: {
420
+ url: field(z.string(), {
421
+ env: "FIRST_TREE_HUB_DATABASE_URL",
422
+ auto: "docker-pg",
423
+ prompt: {
424
+ message: "PostgreSQL:",
425
+ type: "select",
426
+ choices: [{
427
+ name: "Auto-provision via Docker",
428
+ value: "__auto__"
429
+ }, {
430
+ name: "Provide connection URL",
431
+ value: "__input__"
432
+ }]
433
+ }
434
+ }),
435
+ provider: field(z.enum(["docker", "external"]).default("docker"))
436
+ },
437
+ server: {
438
+ port: field(z.number().default(8e3), { env: "FIRST_TREE_HUB_PORT" }),
439
+ host: field(z.string().default("127.0.0.1"), { env: "FIRST_TREE_HUB_HOST" })
440
+ },
441
+ secrets: {
442
+ jwtSecret: field(z.string(), {
443
+ env: "FIRST_TREE_HUB_JWT_SECRET",
444
+ auto: "random:base64url:32",
445
+ secret: true
446
+ }),
447
+ encryptionKey: field(z.string(), {
448
+ env: "FIRST_TREE_HUB_ENCRYPTION_KEY",
449
+ auto: "random:hex:32",
450
+ secret: true
451
+ })
452
+ },
453
+ contextTree: {
454
+ repo: field(z.string(), {
455
+ env: "FIRST_TREE_HUB_CONTEXT_TREE_REPO",
456
+ prompt: { message: "Context Tree repo URL (e.g. https://github.com/org/first-tree):" }
457
+ }),
458
+ branch: field(z.string().default("main")),
459
+ syncInterval: field(z.number().default(60))
460
+ },
461
+ github: {
462
+ token: field(z.string(), {
463
+ env: "FIRST_TREE_HUB_GITHUB_TOKEN",
464
+ secret: true,
465
+ prompt: {
466
+ message: "GitHub token (create at https://github.com/settings/tokens → repo scope):",
467
+ type: "password"
468
+ }
469
+ }),
470
+ webhookSecret: field(z.string().optional(), {
471
+ env: "FIRST_TREE_HUB_GITHUB_WEBHOOK_SECRET",
472
+ secret: true
473
+ })
474
+ },
475
+ cors: optional({ origin: field(z.string(), { env: "FIRST_TREE_HUB_CORS_ORIGIN" }) }),
476
+ rateLimit: optional({
477
+ max: field(z.number().default(100), { env: "FIRST_TREE_HUB_RATE_LIMIT_MAX" }),
478
+ loginMax: field(z.number().default(5), { env: "FIRST_TREE_HUB_RATE_LIMIT_LOGIN_MAX" }),
479
+ webhookMax: field(z.number().default(60), { env: "FIRST_TREE_HUB_RATE_LIMIT_WEBHOOK_MAX" })
480
+ })
481
+ });
482
+ //#endregion
483
+ //#region src/core/bootstrap.ts
484
+ var bootstrap_exports = /* @__PURE__ */ __exportAll({
485
+ bootstrapToken: () => bootstrapToken,
486
+ checkBootstrapStatus: () => checkBootstrapStatus,
487
+ getGitHubToken: () => getGitHubToken,
488
+ getGitHubUsername: () => getGitHubUsername,
489
+ resolveAgentToken: () => resolveAgentToken,
490
+ resolveServerUrl: () => resolveServerUrl
491
+ });
492
+ /**
493
+ * Get the current GitHub username from `gh auth status`.
494
+ */
495
+ function getGitHubUsername() {
496
+ try {
497
+ const output = execSync("gh api /user --jq .login", { encoding: "utf-8" }).trim();
498
+ if (!output) throw new Error("Empty response");
499
+ return output;
500
+ } catch {
501
+ throw new Error("Failed to get GitHub username. Ensure `gh` CLI is installed and authenticated:\n gh auth login");
502
+ }
503
+ }
504
+ /**
505
+ * Get the GitHub auth token from `gh auth token`.
506
+ */
507
+ function getGitHubToken() {
508
+ try {
509
+ const output = execSync("gh auth token", { encoding: "utf-8" }).trim();
510
+ if (!output) throw new Error("Empty response");
511
+ return output;
512
+ } catch {
513
+ throw new Error("Failed to get GitHub token. Ensure `gh` CLI is installed and authenticated:\n gh auth login");
514
+ }
515
+ }
516
+ /**
517
+ * Resolve Hub server URL from flag, env, or config.
518
+ */
519
+ function resolveServerUrl(flagValue) {
520
+ if (flagValue) return flagValue;
521
+ if (process.env.FIRST_TREE_HUB_SERVER) return process.env.FIRST_TREE_HUB_SERVER;
522
+ try {
523
+ const config = getClientConfig();
524
+ if (config.server?.url) return config.server.url;
525
+ } catch {}
526
+ 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>");
527
+ }
528
+ /**
529
+ * Bootstrap a token for an agent using GitHub identity.
530
+ */
531
+ async function bootstrapToken(serverUrl, agentId, options = {}) {
532
+ const githubToken = getGitHubToken();
533
+ const res = await fetch(`${serverUrl}/api/v1/bootstrap/${encodeURIComponent(agentId)}/token`, {
534
+ method: "POST",
535
+ headers: {
536
+ "X-GitHub-Token": githubToken,
537
+ "Content-Type": "application/json"
538
+ },
539
+ body: JSON.stringify({ name: "bootstrap" })
540
+ });
541
+ if (!res.ok) {
542
+ const msg = (await res.json().catch(() => ({}))).error ?? `HTTP ${res.status}`;
543
+ throw new Error(`Bootstrap failed for "${agentId}": ${msg}`);
544
+ }
545
+ const data = await res.json();
546
+ if (options.saveTo === "agent" || !options.saveTo) {
547
+ const configDir = join(homedir(), ".first-tree-hub", "agents", agentId);
548
+ const configPath = `${configDir}/agent.yaml`;
549
+ mkdirSync(configDir, {
550
+ recursive: true,
551
+ mode: 448
552
+ });
553
+ writeFileSync(configPath, `token: "${data.token}"\ntype: claude-code\n`, { mode: 384 });
554
+ chmodSync(configDir, 448);
555
+ } else if (options.saveTo) {
556
+ mkdirSync(dirname(options.saveTo), { recursive: true });
557
+ writeFileSync(options.saveTo, data.token, { mode: 384 });
558
+ }
559
+ return data;
560
+ }
561
+ /**
562
+ * Resolve agent token from FIRST_TREE_HUB_TOKEN env var.
563
+ * Throws if not set.
564
+ */
565
+ function resolveAgentToken() {
566
+ const token = process.env.FIRST_TREE_HUB_TOKEN;
567
+ if (!token) throw new Error("FIRST_TREE_HUB_TOKEN environment variable is required.");
568
+ return token;
569
+ }
570
+ /**
571
+ * Check if an agent exists and is synced.
572
+ */
573
+ async function checkBootstrapStatus(serverUrl, agentId) {
574
+ const githubToken = getGitHubToken();
575
+ const res = await fetch(`${serverUrl}/api/v1/bootstrap/${encodeURIComponent(agentId)}/status`, { headers: { "X-GitHub-Token": githubToken } });
576
+ if (!res.ok) {
577
+ const body = await res.json().catch(() => ({}));
578
+ throw new Error(body.error ?? `HTTP ${res.status}`);
579
+ }
580
+ return await res.json();
581
+ }
582
+ //#endregion
583
+ export { resetConfig as _, getGitHubUsername as a, serverConfigSchema as b, DEFAULT_CONFIG_DIR as c, clientConfigSchema as d, collectMissingPrompts as f, readConfigFile as g, loadAgents as h, getGitHubToken as i, DEFAULT_DATA_DIR as l, initConfig as m, bootstrap_exports as n, resolveAgentToken as o, getConfigValue as p, checkBootstrapStatus as r, resolveServerUrl as s, bootstrapToken as t, agentConfigSchema as u, resetConfigMeta as v, setConfigValue as x, resolveConfigReadonly as y };