2026-05-02 18:14:41 -07:00
|
|
|
import path from "node:path";
|
|
|
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
|
import { config as loadDotenv } from "dotenv";
|
2026-02-13 22:43:55 -08:00
|
|
|
import { z } from "zod";
|
2026-05-02 18:14:41 -07:00
|
|
|
|
|
|
|
|
loadDotenv({ quiet: true });
|
|
|
|
|
loadDotenv({ path: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.env"), quiet: true });
|
|
|
|
|
|
|
|
|
|
const OptionalUrlSchema = z.preprocess(
|
|
|
|
|
(value) => (typeof value === "string" && value.trim() === "" ? undefined : value),
|
|
|
|
|
z.string().trim().url().optional()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const ChatWebSearchEngineSchema = z.preprocess(
|
|
|
|
|
(value) => {
|
|
|
|
|
if (typeof value !== "string") return value;
|
|
|
|
|
const trimmed = value.trim();
|
|
|
|
|
return trimmed ? trimmed.toLowerCase() : undefined;
|
|
|
|
|
},
|
|
|
|
|
z.enum(["exa", "searxng"]).default("exa")
|
|
|
|
|
);
|
2026-02-13 22:43:55 -08:00
|
|
|
|
2026-05-02 19:38:15 -07:00
|
|
|
const BooleanFlagSchema = z.preprocess((value) => {
|
|
|
|
|
if (typeof value !== "string") return value;
|
|
|
|
|
const normalized = value.trim().toLowerCase();
|
|
|
|
|
if (!normalized) return undefined;
|
|
|
|
|
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
|
|
|
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
|
|
|
return value;
|
|
|
|
|
}, z.boolean().default(false));
|
|
|
|
|
|
|
|
|
|
const OptionalTrimmedStringSchema = z.preprocess(
|
|
|
|
|
(value) => (typeof value === "string" && value.trim() === "" ? undefined : value),
|
|
|
|
|
z.string().trim().min(1).optional()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
function defaultedPositiveInt(defaultValue: number) {
|
|
|
|
|
return z.preprocess(
|
|
|
|
|
(value) => (typeof value === "string" && value.trim() === "" ? undefined : value),
|
|
|
|
|
z.coerce.number().int().positive().default(defaultValue)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function defaultedTrimmedString(defaultValue: string) {
|
|
|
|
|
return z.preprocess(
|
|
|
|
|
(value) => (typeof value === "string" && value.trim() === "" ? undefined : value),
|
|
|
|
|
z.string().trim().min(1).default(defaultValue)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-13 22:43:55 -08:00
|
|
|
const EnvSchema = z.object({
|
|
|
|
|
PORT: z.coerce.number().int().positive().default(8787),
|
|
|
|
|
HOST: z.string().default("0.0.0.0"),
|
|
|
|
|
|
|
|
|
|
// simple bearer-token auth for your personal backend
|
|
|
|
|
ADMIN_TOKEN: z.string().min(20).optional(),
|
|
|
|
|
|
|
|
|
|
// provider keys
|
|
|
|
|
OPENAI_API_KEY: z.string().optional(),
|
|
|
|
|
ANTHROPIC_API_KEY: z.string().optional(),
|
|
|
|
|
XAI_API_KEY: z.string().optional(),
|
2026-02-13 23:49:55 -08:00
|
|
|
EXA_API_KEY: z.string().optional(),
|
2026-05-02 18:14:41 -07:00
|
|
|
|
|
|
|
|
// Chat-mode web_search tool configuration. Search mode remains Exa-only for now.
|
|
|
|
|
CHAT_WEB_SEARCH_ENGINE: ChatWebSearchEngineSchema,
|
|
|
|
|
SEARXNG_BASE_URL: OptionalUrlSchema,
|
2026-05-02 19:38:15 -07:00
|
|
|
|
|
|
|
|
// Optional chat-mode Codex tool. When enabled, the server SSHes into a remote
|
|
|
|
|
// devbox and runs `codex exec` in a persistent scratch directory there.
|
|
|
|
|
CHAT_CODEX_TOOL_ENABLED: BooleanFlagSchema,
|
|
|
|
|
CHAT_CODEX_REMOTE_HOST: OptionalTrimmedStringSchema,
|
|
|
|
|
CHAT_CODEX_REMOTE_USER: OptionalTrimmedStringSchema,
|
|
|
|
|
CHAT_CODEX_REMOTE_PORT: defaultedPositiveInt(22),
|
|
|
|
|
CHAT_CODEX_REMOTE_WORKDIR: defaultedTrimmedString("/workspace/sybil-codex"),
|
|
|
|
|
CHAT_CODEX_SSH_KEY_PATH: OptionalTrimmedStringSchema,
|
|
|
|
|
CHAT_CODEX_SSH_PRIVATE_KEY_B64: OptionalTrimmedStringSchema,
|
|
|
|
|
CHAT_CODEX_EXEC_TIMEOUT_MS: defaultedPositiveInt(600_000),
|
2026-05-02 19:52:09 -07:00
|
|
|
|
|
|
|
|
// Optional arbitrary shell tool that runs only on the configured devbox.
|
|
|
|
|
CHAT_SHELL_TOOL_ENABLED: BooleanFlagSchema,
|
|
|
|
|
CHAT_SHELL_EXEC_TIMEOUT_MS: defaultedPositiveInt(120_000),
|
2026-05-02 18:14:41 -07:00
|
|
|
}).superRefine((value, ctx) => {
|
|
|
|
|
if (value.CHAT_WEB_SEARCH_ENGINE === "searxng" && !value.SEARXNG_BASE_URL) {
|
|
|
|
|
ctx.addIssue({
|
|
|
|
|
code: "custom",
|
|
|
|
|
path: ["SEARXNG_BASE_URL"],
|
|
|
|
|
message: "SEARXNG_BASE_URL is required when CHAT_WEB_SEARCH_ENGINE=searxng",
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-05-02 19:38:15 -07:00
|
|
|
|
2026-05-02 19:52:09 -07:00
|
|
|
if ((value.CHAT_CODEX_TOOL_ENABLED || value.CHAT_SHELL_TOOL_ENABLED) && !value.CHAT_CODEX_REMOTE_HOST) {
|
2026-05-02 19:38:15 -07:00
|
|
|
ctx.addIssue({
|
|
|
|
|
code: "custom",
|
|
|
|
|
path: ["CHAT_CODEX_REMOTE_HOST"],
|
2026-05-02 19:52:09 -07:00
|
|
|
message: "CHAT_CODEX_REMOTE_HOST is required when CHAT_CODEX_TOOL_ENABLED=true or CHAT_SHELL_TOOL_ENABLED=true",
|
2026-05-02 19:38:15 -07:00
|
|
|
});
|
|
|
|
|
}
|
2026-02-13 22:43:55 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export type Env = z.infer<typeof EnvSchema>;
|
|
|
|
|
|
|
|
|
|
export const env: Env = EnvSchema.parse(process.env);
|