Something went wrong. Try again.
I'm going to build my own OpenClaw, with blackjack... and bun!
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887/** * core/config.ts – Centralised application configuration. * * All runtime settings are resolved here from three sources (in priority * order): CLI arguments, environment variables / `.env` file, and the JSON * config file at `<workspace>/.piclaw/config.json`. * * The exported constants and mutable variables are consumed throughout the * application: * - WORKSPACE_DIR / STORE_DIR / DATA_DIR → db/, ipc.ts, task-scheduler.ts * - WEB_* → channels/web.ts (HTTP/TLS server setup, auth) * - ASSISTANT_NAME / ASSISTANT_AVATAR → agent-pool.ts, channels/formatting.ts * - WHATSAPP_PHONE → channels/whatsapp.ts * - PUSHOVER_* → channels/pushover.ts * - AGENT_TIMEOUT / BACKGROUND_AGENT_TIMEOUT → agent-pool.ts, runtime.ts * - TRIGGER_PATTERN → router.ts (decides whether to process a message) * - TOOL_OUTPUT_* → db/tool-outputs.ts (retention / cleanup scheduling) * * Setter functions (setAssistantName, etc.) allow the agent-control layer to * update identity settings at runtime without a restart. */
import { resolve } from "path";import { existsSync } from "fs";import { readEnvFile } from "./env.js";import { readJsonConfig, writeJsonConfig } from "./config-store.js";import { createLogger } from "../utils/logger.js";import { getConfiguredLogLevel, parseLogLevel } from "../utils/log-level.js";
// ---------------------------------------------------------------------------// CLI argument parsing helpers.// ---------------------------------------------------------------------------
const CLI_ARGS = process.argv.slice(2);
/** Read a CLI flag value, e.g. `--port 3000` or `--port=3000`. */function readCliArg(name: string, alias?: string): string | undefined { const names = [name, alias].filter(Boolean) as string[]; for (let i = 0; i < CLI_ARGS.length; i += 1) { const arg = CLI_ARGS[i]; for (const flag of names) { if (arg === flag) { return CLI_ARGS[i + 1]; } if (arg.startsWith(`${flag}=`)) { return arg.slice(flag.length + 1); } } } return undefined;}
const CLI_WORKSPACE = readCliArg("--workspace", "-w");
// ---------------------------------------------------------------------------// .env file – loaded once at module init and merged with process.env below.// ---------------------------------------------------------------------------const envConfig = readEnvFile([ "PICLAW_ASSISTANT_NAME", "PICLAW_ASSISTANT_AVATAR", "PICLAW_USER_NAME", "PICLAW_USER_AVATAR", "PICLAW_USER_AVATAR_BACKGROUND", "ASSISTANT_NAME", "ASSISTANT_AVATAR", "PICLAW_AGENT_TIMEOUT", "AGENT_TIMEOUT", "PICLAW_BACKGROUND_AGENT_TIMEOUT", "AGENT_TIMEOUT_BACKGROUND", "PICLAW_WHATSAPP_PHONE", "WHATSAPP_PHONE", "PUSHOVER_APP_TOKEN", "PUSHOVER_USER_KEY", "PUSHOVER_DEVICE", "PUSHOVER_PRIORITY", "PUSHOVER_SOUND", "PICLAW_WEB_TLS_CERT", "PICLAW_WEB_TLS_KEY", "PICLAW_WEB_TOTP_SECRET", "PICLAW_WEB_TOTP_WINDOW", "PICLAW_WEB_SESSION_TTL", "PICLAW_WEB_INTERNAL_SECRET", "PICLAW_WEB_PASSKEY_MODE", "PICLAW_WEB_TERMINAL_ENABLED", "PICLAW_WEB_VNC_ALLOW_DIRECT", "PICLAW_VNC_ALLOW_DIRECT", "PICLAW_WEB_VNC_TARGETS", "PICLAW_VNC_TARGETS", "PICLAW_DEBUG_CARD_SUBMISSIONS", "PICLAW_TRUST_PROXY", "PICLAW_SESSION_MAX_SIZE_MB", "PICLAW_SESSION_AUTO_ROTATE", "PICLAW_WORKSPACE_SEARCH_ROOTS", "PICLAW_INTERNAL_SECRET", "PICLAW_REMOTE_INTEROP_ENABLED", "PICLAW_REMOTE_INTEROP_ALLOW_HTTP", "PICLAW_REMOTE_INSTANCE_NAME", "PICLAW_REMOTE_SHORT_CIRCUIT_ENABLED", "PICLAW_REMOTE_INTEROP_DECISION_MODEL", "PICLAW_LOG_LEVEL", "LOG_LEVEL",]);
import { pickString, pickNumber, pickBoolean, pickStringArray } from "./config-helpers.js";import type { RuntimeTimingConfig } from "./config-helpers.js";export { pickString, pickNumber, pickBoolean, pickStringArray };export type { RuntimeTimingConfig };
// ---------------------------------------------------------------------------// Timing constants used by the runtime message loop and scheduler.// ---------------------------------------------------------------------------
/** Grouped runtime timing settings. */export const RUNTIME_TIMING_CONFIG = Object.freeze<RuntimeTimingConfig>({ pollIntervalMs: 2000, schedulerPollIntervalMs: 60000, ipcPollIntervalMs: 1000, timezone: process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone,});
/** Return grouped runtime timing settings for runtime wiring and tests. */export function getRuntimeTimingConfig(): Readonly<RuntimeTimingConfig> { return RUNTIME_TIMING_CONFIG;}
// ---------------------------------------------------------------------------// Filesystem paths – all env-configurable for flexible volume layouts.// Defaults assume /workspace is the persistent external volume.// ---------------------------------------------------------------------------
/** Root of the persistent workspace (bind-mounted volume). */export const WORKSPACE_DIR = resolve(CLI_WORKSPACE || process.env.PICLAW_WORKSPACE || "/workspace");/** Directory for the SQLite database and related state. */export const STORE_DIR = resolve( CLI_WORKSPACE ? `${WORKSPACE_DIR}/.piclaw/store` : (process.env.PICLAW_STORE || `${WORKSPACE_DIR}/.piclaw/store`));/** Directory for runtime data (sessions, IPC files, etc.). */export const DATA_DIR = resolve( CLI_WORKSPACE ? `${WORKSPACE_DIR}/.piclaw/data` : (process.env.PICLAW_DATA || `${WORKSPACE_DIR}/.piclaw/data`));
// ---------------------------------------------------------------------------// TLS – optional HTTPS support for the web channel.// ---------------------------------------------------------------------------
const DEFAULT_TLS_CERT_PATH = resolve(WORKSPACE_DIR, ".piclaw", "certs", "sandbox.local.crt");const DEFAULT_TLS_KEY_PATH = resolve(WORKSPACE_DIR, ".piclaw", "certs", "sandbox.local.key");/** True when default self-signed TLS certificates exist on disk. */const HAS_DEFAULT_TLS = existsSync(DEFAULT_TLS_CERT_PATH) && existsSync(DEFAULT_TLS_KEY_PATH);
// ---------------------------------------------------------------------------// JSON config file – loaded once and merged with env/CLI values below.// ---------------------------------------------------------------------------
/** Absolute path to the JSON config file. */export const PICLAW_CONFIG_PATH = resolve(WORKSPACE_DIR, ".piclaw", "config.json");const piclawConfig = readJsonConfig(PICLAW_CONFIG_PATH);
// Sub-objects inside the config file for namespaced settings.const pushoverConfig = piclawConfig.pushover && typeof piclawConfig.pushover === "object" ? (piclawConfig.pushover as Record<string, unknown>) : piclawConfig;const assistantConfig = piclawConfig.assistant && typeof piclawConfig.assistant === "object" ? (piclawConfig.assistant as Record<string, unknown>) : piclawConfig;const userConfig = piclawConfig.user && typeof piclawConfig.user === "object" ? (piclawConfig.user as Record<string, unknown>) : piclawConfig;const webConfig = piclawConfig.web && typeof piclawConfig.web === "object" ? (piclawConfig.web as Record<string, unknown>) : piclawConfig;const toolsConfig = piclawConfig.tools && typeof piclawConfig.tools === "object" ? (piclawConfig.tools as Record<string, unknown>) : piclawConfig;
// Extract individual settings from the JSON config, trying multiple key aliases.const configAppToken = pickString(pushoverConfig, ["appToken", "app_token", "PUSHOVER_APP_TOKEN"]);const configUserKey = pickString(pushoverConfig, ["userKey", "user_key", "PUSHOVER_USER_KEY"]);const configDevice = pickString(pushoverConfig, ["device", "PUSHOVER_DEVICE"]);const configSound = pickString(pushoverConfig, ["sound", "PUSHOVER_SOUND"]);const configPriority = pickNumber(pushoverConfig, ["priority", "PUSHOVER_PRIORITY"]);const configWhatsappPhone = pickString(piclawConfig, ["whatsappPhone", "whatsapp_phone", "WHATSAPP_PHONE"]);const configAssistantName = pickString(assistantConfig, [ "assistantName", "assistant_name", "agentName", "agent_name", "name", "ASSISTANT_NAME",]);const configAssistantAvatar = pickString(assistantConfig, [ "assistantAvatar", "assistant_avatar", "agentAvatar", "agent_avatar", "avatar", "ASSISTANT_AVATAR",]);const configUserName = pickString(userConfig, [ "userName", "user_name", "name", "PICLAW_USER_NAME",]);const configUserAvatar = pickString(userConfig, [ "userAvatar", "user_avatar", "avatar", "PICLAW_USER_AVATAR",]);const configUserAvatarBackground = pickString(userConfig, [ "userAvatarBackground", "user_avatar_background", "userAvatarBg", "user_avatar_bg", "avatarBackground", "avatar_background", "PICLAW_USER_AVATAR_BACKGROUND",]);const configWebTotpSecret = pickString(webConfig, [ "totpSecret", "totp_secret", "webTotpSecret", "web_totp_secret", "PICLAW_WEB_TOTP_SECRET", "PICLAW_TOTP_SECRET",]);const configWebTotpWindow = pickNumber(webConfig, [ "totpWindow", "totp_window", "webTotpWindow", "web_totp_window", "PICLAW_WEB_TOTP_WINDOW",]);const configWebSessionTtl = pickNumber(webConfig, [ "sessionTtl", "session_ttl", "webSessionTtl", "web_session_ttl", "PICLAW_WEB_SESSION_TTL",]);const configWebInternalSecret = pickString(webConfig, [ "internalSecret", "internal_secret", "webInternalSecret", "web_internal_secret", "PICLAW_WEB_INTERNAL_SECRET", "PICLAW_INTERNAL_SECRET",]);const configWebPasskeyMode = pickString(webConfig, [ "passkeyMode", "passkey_mode", "webPasskeyMode", "web_passkey_mode", "PICLAW_WEB_PASSKEY_MODE",]);const configTrustProxy = pickBoolean(webConfig, [ "trustProxy", "trust_proxy", "PICLAW_TRUST_PROXY",]);
// ---------------------------------------------------------------------------// Deprecation warnings for renamed environment variables.// ---------------------------------------------------------------------------
const log = createLogger("core.config");
/** Emit a structured warning if only the old env var name is set. */function warnDeprecatedEnv(oldName: string, newName: string): void { const oldValue = process.env[oldName] ?? envConfig[oldName]; const newValue = process.env[newName] ?? envConfig[newName]; if (oldValue && !newValue) { log.warn("Deprecated environment variable is set", { operation: "core_config.warn_deprecated_env", oldName, newName, }); }}
warnDeprecatedEnv("ASSISTANT_NAME", "PICLAW_ASSISTANT_NAME");warnDeprecatedEnv("ASSISTANT_AVATAR", "PICLAW_ASSISTANT_AVATAR");warnDeprecatedEnv("AGENT_TIMEOUT", "PICLAW_AGENT_TIMEOUT");warnDeprecatedEnv("AGENT_TIMEOUT_BACKGROUND", "PICLAW_BACKGROUND_AGENT_TIMEOUT");warnDeprecatedEnv("LOG_LEVEL", "PICLAW_LOG_LEVEL");
// ---------------------------------------------------------------------------// Mutable identity settings – can be changed at runtime via agent-control.// ---------------------------------------------------------------------------
/** Typed logging settings grouped for runtime diagnostics. */export interface LoggingConfig { level: ReturnType<typeof parseLogLevel>;}
/** Grouped logging settings. */export const LOGGING_CONFIG = Object.freeze<LoggingConfig>({ level: parseLogLevel( process.env.PICLAW_LOG_LEVEL || envConfig.PICLAW_LOG_LEVEL || process.env.LOG_LEVEL || envConfig.LOG_LEVEL || getConfiguredLogLevel(), ),});
/** Return grouped logging settings for runtime wiring and tests. */export function getLoggingConfig(): Readonly<LoggingConfig> { return LOGGING_CONFIG;}
/** Typed mutable identity settings grouped for runtime consumers that need live values. */export interface IdentityConfig { assistantName: string; assistantAvatar: string; userName: string; userAvatar: string; userAvatarBackground: string;}
/** Grouped mutable identity settings. Legacy flat exports below stay in sync for compatibility. */export const IDENTITY_CONFIG: IdentityConfig = Object.seal({ assistantName: process.env.PICLAW_ASSISTANT_NAME || envConfig.PICLAW_ASSISTANT_NAME || process.env.ASSISTANT_NAME || envConfig.ASSISTANT_NAME || configAssistantName || "PiClaw", assistantAvatar: process.env.PICLAW_ASSISTANT_AVATAR || envConfig.PICLAW_ASSISTANT_AVATAR || process.env.ASSISTANT_AVATAR || envConfig.ASSISTANT_AVATAR || configAssistantAvatar || "", userName: process.env.PICLAW_USER_NAME || envConfig.PICLAW_USER_NAME || configUserName || "", userAvatar: process.env.PICLAW_USER_AVATAR || envConfig.PICLAW_USER_AVATAR || configUserAvatar || "", userAvatarBackground: process.env.PICLAW_USER_AVATAR_BACKGROUND || envConfig.PICLAW_USER_AVATAR_BACKGROUND || configUserAvatarBackground || "",});
/** Return grouped mutable identity settings for runtime wiring and tests. */export function getIdentityConfig(): Readonly<IdentityConfig> { return IDENTITY_CONFIG;}
/** Display name of the assistant. Updated by setAssistantName(). */export let ASSISTANT_NAME = IDENTITY_CONFIG.assistantName;
/** URL or path to the assistant's avatar image. Updated by setAssistantAvatar(). */export let ASSISTANT_AVATAR = IDENTITY_CONFIG.assistantAvatar;
/** Display name for the human user in the web UI. */export let USER_NAME = IDENTITY_CONFIG.userName;
/** URL or path to the user's avatar image. */export let USER_AVATAR = IDENTITY_CONFIG.userAvatar;
/** CSS background colour for the user avatar circle. */export let USER_AVATAR_BACKGROUND = IDENTITY_CONFIG.userAvatarBackground;
// ---------------------------------------------------------------------------// Agent timeout settings – how long a single agent turn may run.// ---------------------------------------------------------------------------
/** Typed agent turn timeout settings grouped for runtime and handler wiring. */export interface AgentRuntimeConfig { timeoutMs: number; backgroundTimeoutMs: number;}
/** Grouped agent turn timeout settings. */export const AGENT_RUNTIME_CONFIG = Object.freeze<AgentRuntimeConfig>({ timeoutMs: parseInt( process.env.PICLAW_AGENT_TIMEOUT || envConfig.PICLAW_AGENT_TIMEOUT || process.env.AGENT_TIMEOUT || envConfig.AGENT_TIMEOUT || "3600000", 10 ), backgroundTimeoutMs: parseInt( process.env.PICLAW_BACKGROUND_AGENT_TIMEOUT || envConfig.PICLAW_BACKGROUND_AGENT_TIMEOUT || process.env.AGENT_TIMEOUT_BACKGROUND || envConfig.AGENT_TIMEOUT_BACKGROUND || "0", 10 ),});
/** Return grouped agent timeout settings for runtime wiring and tests. */export function getAgentRuntimeConfig(): Readonly<AgentRuntimeConfig> { return AGENT_RUNTIME_CONFIG;}
/** Parse a numeric port string, falling back to `fallback` on failure. */function parsePort(value: string | undefined, fallback: number): number { if (!value) return fallback; const parsed = parseInt(value, 10); return Number.isNaN(parsed) ? fallback : parsed;}
// ---------------------------------------------------------------------------// Web channel configuration (HTTP server, TLS, auth).// ---------------------------------------------------------------------------
const ENV_WEB_PORT = parseInt(process.env.PICLAW_WEB_PORT || "8080", 10);const CLI_WEB_PORT = readCliArg("--port", "-p");const CLI_WEB_HOST = readCliArg("--host");const ENV_WEB_IDLE_TIMEOUT = parseInt(process.env.PICLAW_WEB_IDLE_TIMEOUT || "0", 10);const CLI_WEB_IDLE_TIMEOUT = readCliArg("--idle-timeout");const CLI_WEB_TLS_CERT = readCliArg("--tls-cert");const CLI_WEB_TLS_KEY = readCliArg("--tls-key");
/** Typed web server network/TLS settings grouped for WebChannel wiring. */export interface WebServerConfig { port: number; host: string; idleTimeout: number; tlsCert: string; tlsKey: string;}
/** Grouped web server network/TLS settings. */export const WEB_SERVER_CONFIG = Object.freeze<WebServerConfig>({ port: parsePort(CLI_WEB_PORT, ENV_WEB_PORT), host: CLI_WEB_HOST || process.env.PICLAW_WEB_HOST || "0.0.0.0", idleTimeout: parsePort(CLI_WEB_IDLE_TIMEOUT, ENV_WEB_IDLE_TIMEOUT), tlsCert: CLI_WEB_TLS_CERT || process.env.PICLAW_WEB_TLS_CERT || envConfig.PICLAW_WEB_TLS_CERT || (HAS_DEFAULT_TLS ? DEFAULT_TLS_CERT_PATH : ""), tlsKey: CLI_WEB_TLS_KEY || process.env.PICLAW_WEB_TLS_KEY || envConfig.PICLAW_WEB_TLS_KEY || (HAS_DEFAULT_TLS ? DEFAULT_TLS_KEY_PATH : ""),});
/** Return grouped web server settings for WebChannel wiring and tests. */export function getWebServerConfig(): Readonly<WebServerConfig> { return WEB_SERVER_CONFIG;}
/** Mutable web auth/session/runtime settings grouped for auth and UI wiring. */export interface WebRuntimeConfig { totpSecret: string; totpWindow: number; sessionTtl: number; internalSecret: string; passkeyMode: string; terminalEnabled: boolean; vncAllowDirect: boolean; vncTargetsRaw: string; debugCardSubmissions: boolean; trustProxy: boolean;}
export function isDefaultWebTerminalEnabled(platform = process.platform): boolean { return platform === "linux" || platform === "darwin";}
export function isDefaultWebVncDirectEnabled(platform = process.platform): boolean { return platform === "linux" || platform === "darwin" || platform === "win32";}
const nestedWebTerminalEnabled = pickBoolean(webConfig, ["terminalEnabled", "webTerminalEnabled", "PICLAW_WEB_TERMINAL_ENABLED"]);const legacyWebTerminalEnabled = pickBoolean(piclawConfig, ["webTerminalEnabled"]);const envWebTerminalEnabled = pickBoolean({ PICLAW_WEB_TERMINAL_ENABLED: process.env.PICLAW_WEB_TERMINAL_ENABLED ?? envConfig.PICLAW_WEB_TERMINAL_ENABLED }, ["PICLAW_WEB_TERMINAL_ENABLED"]);const nestedWebVncAllowDirect = pickBoolean(webConfig, ["vncAllowDirect", "vnc_allow_direct", "webVncAllowDirect", "PICLAW_WEB_VNC_ALLOW_DIRECT", "PICLAW_VNC_ALLOW_DIRECT"]);const legacyWebVncAllowDirect = pickBoolean(piclawConfig, ["webVncAllowDirect"]);const envWebVncAllowDirect = pickBoolean({ PICLAW_WEB_VNC_ALLOW_DIRECT: process.env.PICLAW_WEB_VNC_ALLOW_DIRECT ?? envConfig.PICLAW_WEB_VNC_ALLOW_DIRECT ?? process.env.PICLAW_VNC_ALLOW_DIRECT ?? envConfig.PICLAW_VNC_ALLOW_DIRECT }, ["PICLAW_WEB_VNC_ALLOW_DIRECT"]);const nestedWebVncTargets = pickString(webConfig, ["vncTargets", "vnc_targets", "webVncTargets", "PICLAW_WEB_VNC_TARGETS", "PICLAW_VNC_TARGETS"]);const legacyWebVncTargets = pickString(piclawConfig, ["webVncTargets"]);const debugCards = pickBoolean(piclawConfig, ["debugCardSubmissions", "PICLAW_DEBUG_CARD_SUBMISSIONS"]);const envDebugCards = pickBoolean({ PICLAW_DEBUG_CARD_SUBMISSIONS: process.env.PICLAW_DEBUG_CARD_SUBMISSIONS ?? envConfig.PICLAW_DEBUG_CARD_SUBMISSIONS }, ["PICLAW_DEBUG_CARD_SUBMISSIONS"]);const envTrustProxyRaw = process.env.PICLAW_TRUST_PROXY ?? envConfig.PICLAW_TRUST_PROXY;const envTrustProxy = pickBoolean({ PICLAW_TRUST_PROXY: envTrustProxyRaw }, ["PICLAW_TRUST_PROXY"]);
/** Grouped web auth/session/runtime settings. `totpSecret` stays mutable for runtime resets. */export const WEB_RUNTIME_CONFIG: WebRuntimeConfig = Object.seal({ totpSecret: process.env.PICLAW_WEB_TOTP_SECRET || envConfig.PICLAW_WEB_TOTP_SECRET || configWebTotpSecret || "", totpWindow: parseInt( process.env.PICLAW_WEB_TOTP_WINDOW || envConfig.PICLAW_WEB_TOTP_WINDOW || (configWebTotpWindow !== undefined ? String(configWebTotpWindow) : "1"), 10 ), sessionTtl: parseInt( process.env.PICLAW_WEB_SESSION_TTL || envConfig.PICLAW_WEB_SESSION_TTL || (configWebSessionTtl !== undefined ? String(configWebSessionTtl) : String(7 * 24 * 60 * 60)), 10 ), internalSecret: process.env.PICLAW_INTERNAL_SECRET || process.env.PICLAW_WEB_INTERNAL_SECRET || envConfig.PICLAW_INTERNAL_SECRET || envConfig.PICLAW_WEB_INTERNAL_SECRET || configWebInternalSecret || "", passkeyMode: ( process.env.PICLAW_WEB_PASSKEY_MODE || envConfig.PICLAW_WEB_PASSKEY_MODE || configWebPasskeyMode || "totp-fallback" ).toLowerCase(), terminalEnabled: envWebTerminalEnabled ?? nestedWebTerminalEnabled ?? legacyWebTerminalEnabled ?? isDefaultWebTerminalEnabled(), vncAllowDirect: envWebVncAllowDirect ?? nestedWebVncAllowDirect ?? legacyWebVncAllowDirect ?? isDefaultWebVncDirectEnabled(), vncTargetsRaw: process.env.PICLAW_WEB_VNC_TARGETS || envConfig.PICLAW_WEB_VNC_TARGETS || process.env.PICLAW_VNC_TARGETS || envConfig.PICLAW_VNC_TARGETS || nestedWebVncTargets || legacyWebVncTargets || "", debugCardSubmissions: envDebugCards ?? debugCards ?? false, trustProxy: envTrustProxy ?? configTrustProxy ?? false,});
/** Return grouped web auth/session/runtime settings for handlers and tests. */export function getWebRuntimeConfig(): Readonly<WebRuntimeConfig> { return WEB_RUNTIME_CONFIG;}
// ---------------------------------------------------------------------------// Remote interop configuration (cross-instance IPC).// ---------------------------------------------------------------------------
/** Typed remote interop settings grouped for lower-coupling service wiring. */export interface RemoteInteropConfig { enabled: boolean; allowHttp: boolean; shortCircuitEnabled: boolean; instanceName: string; decisionModel: string;}
const remoteInteropEnabledRaw = pickBoolean(piclawConfig, ["remoteInteropEnabled", "PICLAW_REMOTE_INTEROP_ENABLED"]);const remoteInteropAllowHttpRaw = pickBoolean(piclawConfig, ["remoteInteropAllowHttp", "PICLAW_REMOTE_INTEROP_ALLOW_HTTP"]);const remoteShortCircuitRaw = pickBoolean(piclawConfig, ["remoteInteropShortCircuitEnabled", "PICLAW_REMOTE_SHORT_CIRCUIT_ENABLED"]);
/** Grouped remote interop settings with existing config/env precedence preserved. */export const REMOTE_INTEROP_CONFIG = Object.freeze<RemoteInteropConfig>({ enabled: remoteInteropEnabledRaw ?? ((process.env.PICLAW_REMOTE_INTEROP_ENABLED || "").toLowerCase() === "true" || process.env.PICLAW_REMOTE_INTEROP_ENABLED === "1"), allowHttp: remoteInteropAllowHttpRaw ?? ((process.env.PICLAW_REMOTE_INTEROP_ALLOW_HTTP || "").toLowerCase() === "true" || process.env.PICLAW_REMOTE_INTEROP_ALLOW_HTTP === "1"), shortCircuitEnabled: remoteShortCircuitRaw ?? ((process.env.PICLAW_REMOTE_SHORT_CIRCUIT_ENABLED || "").toLowerCase() === "true" || process.env.PICLAW_REMOTE_SHORT_CIRCUIT_ENABLED === "1"), instanceName: pickString(piclawConfig, ["remoteInstanceName", "PICLAW_REMOTE_INSTANCE_NAME"]) || process.env.PICLAW_REMOTE_INSTANCE_NAME || "", decisionModel: pickString(piclawConfig, ["remoteInteropDecisionModel", "PICLAW_REMOTE_INTEROP_DECISION_MODEL"]) || process.env.PICLAW_REMOTE_INTEROP_DECISION_MODEL || "",});
/** Return the grouped remote interop settings for service wiring and tests. */export function getRemoteInteropConfig(): Readonly<RemoteInteropConfig> { return REMOTE_INTEROP_CONFIG;}
/** Directory for persisted Pi session files. */export const SESSIONS_DIR = resolve(DATA_DIR, "sessions");
const configSessionMaxSizeMb = pickNumber(piclawConfig, [ "sessionMaxSizeMb", "session_max_size_mb", "PICLAW_SESSION_MAX_SIZE_MB",]);const configSessionAutoRotate = pickBoolean(piclawConfig, [ "sessionAutoRotate", "session_auto_rotate", "PICLAW_SESSION_AUTO_ROTATE",]);const configAdditionalDefaultTools = pickStringArray(toolsConfig, [ "additionalDefaultTools", "additional_default_tools", "PICLAW_ADDITIONAL_DEFAULT_TOOLS",]);const configWorkspaceSearchRoots = pickStringArray(toolsConfig, [ "workspaceSearchRoots", "workspace_search_roots", "PICLAW_WORKSPACE_SEARCH_ROOTS",]);const configWorkspaceSearchExtensions = pickStringArray(toolsConfig, [ "workspaceSearchExtensions", "workspace_search_extensions", "PICLAW_WORKSPACE_SEARCH_EXTENSIONS",]);
/** Typed session-file safeguards grouped for runtime/session wiring. */export interface SessionStorageConfig { maxSizeMb: number; maxSizeBytes: number; autoRotate: boolean;}
const sessionMaxSizeMb = pickNumber({ PICLAW_SESSION_MAX_SIZE_MB: process.env.PICLAW_SESSION_MAX_SIZE_MB ?? envConfig.PICLAW_SESSION_MAX_SIZE_MB }, [ "PICLAW_SESSION_MAX_SIZE_MB", ]) ?? configSessionMaxSizeMb ?? 100;
/** Grouped session-file safeguards. */export const SESSION_STORAGE_CONFIG = Object.freeze<SessionStorageConfig>({ maxSizeMb: sessionMaxSizeMb, maxSizeBytes: sessionMaxSizeMb * 1024 * 1024, autoRotate: pickBoolean({ PICLAW_SESSION_AUTO_ROTATE: process.env.PICLAW_SESSION_AUTO_ROTATE ?? envConfig.PICLAW_SESSION_AUTO_ROTATE }, [ "PICLAW_SESSION_AUTO_ROTATE", ]) ?? configSessionAutoRotate ?? false,});
/** Return grouped session-file safeguards for runtime wiring and tests. */export function getSessionStorageConfig(): Readonly<SessionStorageConfig> { return SESSION_STORAGE_CONFIG;}
// ---------------------------------------------------------------------------// Tool activation defaults – used by lazy tool activation.// ---------------------------------------------------------------------------
/** Typed tool-activation config grouped for default active-tool selection. */export interface ToolActivationConfig { additionalDefaultTools: string[];}
/** Grouped tool-activation config loaded from `.piclaw/config.json`. */export const TOOL_ACTIVATION_CONFIG = Object.freeze<ToolActivationConfig>({ additionalDefaultTools: configAdditionalDefaultTools ?? [],});
/** Typed workspace-search config grouped for FTS root and extension selection. */export interface WorkspaceSearchConfig { roots: string[]; /** Additional file extensions to index (merged with built-in defaults). */ extraExtensions: string[];}
const workspaceSearchRoots = pickStringArray( { PICLAW_WORKSPACE_SEARCH_ROOTS: process.env.PICLAW_WORKSPACE_SEARCH_ROOTS ?? envConfig.PICLAW_WORKSPACE_SEARCH_ROOTS }, ["PICLAW_WORKSPACE_SEARCH_ROOTS"],) ?? configWorkspaceSearchRoots ?? ["notes", ".pi/skills"];
const workspaceSearchExtensions = pickStringArray( { PICLAW_WORKSPACE_SEARCH_EXTENSIONS: process.env.PICLAW_WORKSPACE_SEARCH_EXTENSIONS ?? envConfig.PICLAW_WORKSPACE_SEARCH_EXTENSIONS }, ["PICLAW_WORKSPACE_SEARCH_EXTENSIONS"],) ?? configWorkspaceSearchExtensions ?? [];
/** Grouped workspace-search config loaded from env/config. */export const WORKSPACE_SEARCH_CONFIG = Object.freeze<WorkspaceSearchConfig>({ roots: workspaceSearchRoots, extraExtensions: workspaceSearchExtensions,});
/** Return grouped workspace-search config for runtime wiring and tests. */export function getWorkspaceSearchConfig(): Readonly<WorkspaceSearchConfig> { return WORKSPACE_SEARCH_CONFIG;}
/** Return grouped tool-activation config for runtime wiring and tests. */export function getToolActivationConfig(): Readonly<ToolActivationConfig> { return TOOL_ACTIVATION_CONFIG;}
// ---------------------------------------------------------------------------// Trigger pattern – used by router.ts to decide if a message mentions the bot.// ---------------------------------------------------------------------------
/** Escape special regex characters in a literal string. */function escapeRegex(str: string): string { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");}
/** Mutable routing settings grouped for live assistant-name updates. */export interface RoutingConfig { triggerPattern: RegExp;}
/** Grouped routing settings. `triggerPattern` stays mutable with assistant renames. */export const ROUTING_CONFIG: RoutingConfig = Object.seal({ triggerPattern: new RegExp(`(?:^|\\s)@${escapeRegex(IDENTITY_CONFIG.assistantName)}\\b`, "i"),});
/** Return grouped routing settings for runtime wiring and tests. */export function getRoutingConfig(): Readonly<RoutingConfig> { return ROUTING_CONFIG;}
// ---------------------------------------------------------------------------// Runtime setters – called by agent-control handlers to update identity.// ---------------------------------------------------------------------------
/** Update the assistant's display name and re-derive the trigger pattern. */export function setAssistantName(name: string): void { IDENTITY_CONFIG.assistantName = name.trim() || "PiClaw"; ASSISTANT_NAME = IDENTITY_CONFIG.assistantName; ROUTING_CONFIG.triggerPattern = new RegExp(`(?:^|\\s)@${escapeRegex(IDENTITY_CONFIG.assistantName)}\\b`, "i");}
/** Update the assistant's avatar URL/path. */export function setAssistantAvatar(avatar: string): void { IDENTITY_CONFIG.assistantAvatar = avatar.trim(); ASSISTANT_AVATAR = IDENTITY_CONFIG.assistantAvatar;}
/** Update the human user's display name. */export function setUserName(name: string): void { IDENTITY_CONFIG.userName = name.trim(); USER_NAME = IDENTITY_CONFIG.userName;}
/** Update the human user's avatar URL/path. */export function setUserAvatar(avatar: string): void { IDENTITY_CONFIG.userAvatar = avatar.trim(); USER_AVATAR = IDENTITY_CONFIG.userAvatar;}
/** Update the human user's avatar background colour. */export function setUserAvatarBackground(background: string): void { IDENTITY_CONFIG.userAvatarBackground = background.trim(); USER_AVATAR_BACKGROUND = IDENTITY_CONFIG.userAvatarBackground;}
/** * Rotate/redefine the web-login TOTP secret and persist it to config.json. * * If a runtime secret env var exists, we update it so the new value takes effect * immediately in the same process. Persistence remains in `.piclaw/config.json` * under the `web.totpSecret` key. */export function setWebTotpSecret(secret: string): string { const next = (secret || "").trim();
const config = readJsonConfig(PICLAW_CONFIG_PATH); const web = config.web && typeof config.web === "object" ? { ...(config.web as Record<string, unknown>) } : {}; const totpKeys = [ "totpSecret", "totp_secret", "webTotpSecret", "web_totp_secret", "PICLAW_WEB_TOTP_SECRET", "PICLAW_TOTP_SECRET", ];
for (const key of totpKeys) { delete web[key]; }
if (next) { web.totpSecret = next; }
if (Object.keys(web).length > 0) { config.web = web; } else { delete config.web; }
writeJsonConfig(PICLAW_CONFIG_PATH, config);
WEB_RUNTIME_CONFIG.totpSecret = next; if (next) { process.env.PICLAW_WEB_TOTP_SECRET = next; } else { delete process.env.PICLAW_WEB_TOTP_SECRET; }
return WEB_RUNTIME_CONFIG.totpSecret;}
// ---------------------------------------------------------------------------// Tool output retention settings – used by db/tool-outputs.ts.// ---------------------------------------------------------------------------
/** Typed tool-output retention settings grouped for runtime startup wiring. */export interface ToolOutputConfig { retentionDays: number; cleanupIntervalMs: number;}
/** Grouped tool-output retention settings. */export const TOOL_OUTPUT_CONFIG = Object.freeze<ToolOutputConfig>({ retentionDays: parseInt(process.env.PICLAW_TOOL_OUTPUT_RETENTION_DAYS || "30", 10), cleanupIntervalMs: parseInt( process.env.PICLAW_TOOL_OUTPUT_CLEANUP_INTERVAL_MS || String(12 * 60 * 60 * 1000), 10 ),});
/** Return the grouped tool-output settings for startup wiring and tests. */export function getToolOutputConfig(): Readonly<ToolOutputConfig> { return TOOL_OUTPUT_CONFIG;}
// ---------------------------------------------------------------------------// WhatsApp channel settings.// ---------------------------------------------------------------------------
/** Typed WhatsApp channel settings grouped for startup/channel wiring. */export interface WhatsAppConfig { phoneNumber: string;}
/** Grouped WhatsApp channel settings. */export const WHATSAPP_CONFIG = Object.freeze<WhatsAppConfig>({ phoneNumber: process.env.WHATSAPP_PHONE || envConfig.WHATSAPP_PHONE || process.env.PICLAW_WHATSAPP_PHONE || envConfig.PICLAW_WHATSAPP_PHONE || configWhatsappPhone || "",});
/** Return the grouped WhatsApp settings for startup and channel wiring. */export function getWhatsAppConfig(): Readonly<WhatsAppConfig> { return WHATSAPP_CONFIG;}
// ---------------------------------------------------------------------------// Pushover notification channel settings.// ---------------------------------------------------------------------------
/** Typed Pushover channel settings grouped for runtime startup wiring. */export interface PushoverConfig { appToken: string; userKey: string; device: string; priority: number; sound: string;}
/** Grouped Pushover channel settings. */export const PUSHOVER_CONFIG = Object.freeze<PushoverConfig>({ appToken: process.env.PUSHOVER_APP_TOKEN || envConfig.PUSHOVER_APP_TOKEN || configAppToken || "", userKey: process.env.PUSHOVER_USER_KEY || envConfig.PUSHOVER_USER_KEY || configUserKey || "", device: process.env.PUSHOVER_DEVICE || envConfig.PUSHOVER_DEVICE || configDevice || "", priority: parseInt( process.env.PUSHOVER_PRIORITY || envConfig.PUSHOVER_PRIORITY || (configPriority !== undefined ? String(configPriority) : "0"), 10 ), sound: process.env.PUSHOVER_SOUND || envConfig.PUSHOVER_SOUND || configSound || "",});
/** Return the grouped Pushover settings for startup wiring and tests. */export function getPushoverConfig(): Readonly<PushoverConfig> { return PUSHOVER_CONFIG;}