Add DisclawConfigSchema and AgentYamlSchema with Zod validation. loader.ts and identity.ts validate external data at parse time. Clear error messages with field name and expected type. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
98 lines
2.8 KiB
TypeScript
98 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";
|
|
|
|
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;
|
|
}
|