disclaw/src/config/loader.ts
Nick Tabeling 90f96bdbf4
Some checks failed
CI / build-and-test (ubuntu-latest) (pull_request) Has been cancelled
CI / build-and-test (windows-latest) (pull_request) Has been cancelled
CI / lint (pull_request) Has been cancelled
fix: add local type imports for AgentIdentity and DisclawConfig
Re-exported types via 'export type { X } from' are not available
as local names in the same file. Add explicit 'import type' so
TypeScript resolves the types used in function signatures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 09:51:15 +02:00

99 lines
2.8 KiB
TypeScript

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;
}