import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import * as dotenv from "dotenv"; import * as YAML from "yaml"; import { ZodError } from "zod"; import { childLogger } from "../runtime/logger"; import { DisclawConfigSchema } from "./schema"; import type { DisclawConfig } from "./schema"; export type { DisclawConfig } from "./schema"; const log = childLogger("config"); export interface EnvConfig { DISCORD_BOT_TOKEN: string; DISCORD_GUILD_ID?: string; CLAUDE_PATH?: string; } /** * Expands a leading `~` in a path to the user's home directory and resolves * the result to an absolute, normalised path. No shell expansion is used. * * Exported as a pure function so it can be unit-tested independently. */ export function expandWorkspacesRoot(raw: string, repoRoot: string): string { let expanded = raw; if (expanded.startsWith("~")) { expanded = os.homedir() + expanded.slice(1); } const resolved = path.resolve(repoRoot, expanded); const rel = path.relative(repoRoot, resolved); const isInsideRepo = !rel.startsWith("..") && !path.isAbsolute(rel); if (isInsideRepo) { log.warn( { workspacesRoot: resolved }, "workspaces_root lies inside the DisClaw repository. " + "Claude Code walks parent directories and concatenates every CLAUDE.md it finds. " + "A workspace under the repo root will inherit the project CLAUDE.md, " + "leaking DisClaw system instructions into every agent context. " + "Set workspaces_root to a path outside the repository (e.g. ~/.disclaw/workspaces)." ); } return resolved; } export function loadEnv(rootDir: string): EnvConfig { dotenv.config({ path: path.join(rootDir, ".env") }); const token = process.env.DISCORD_BOT_TOKEN; if (!token) { throw new Error("DISCORD_BOT_TOKEN is not set in .env"); } return { DISCORD_BOT_TOKEN: token, DISCORD_GUILD_ID: process.env.DISCORD_GUILD_ID, CLAUDE_PATH: process.env.CLAUDE_PATH, }; } export function loadDisclawConfig(rootDir: string): DisclawConfig { const configPath = path.join(rootDir, "disclaw.yaml"); if (!fs.existsSync(configPath)) { throw new Error(`disclaw.yaml not found at ${configPath}`); } const raw = fs.readFileSync(configPath, "utf-8"); const rawParsed = YAML.parse(raw); let validated: DisclawConfig; try { validated = DisclawConfigSchema.parse(rawParsed); } catch (err) { if (err instanceof ZodError) { const first = err.errors[0]; const fieldPath = first.path.length > 0 ? first.path.join(".") : "(root)"; throw new Error( `Invalid disclaw.yaml: field "${fieldPath}" -- ${first.message}` ); } throw err; } const config: DisclawConfig = { ...validated, workspaces_root: expandWorkspacesRoot(validated.workspaces_root, rootDir), }; return config; }