Files
clawdbot/src/gateway/server-startup.ts
Gustavo Madeira Santana acb523de86 CLI: streamline startup paths and env parsing
Add shared parseBooleanValue()/isTruthyEnvValue() and apply across CLI, gateway, memory, and live-test flags for consistent env handling.
Introduce route-first fast paths, lazy subcommand registration, and deferred plugin loading to reduce CLI startup overhead.
Centralize config validation via ensureConfigReady() and add config caching/deferred shell env fallback for fewer IO passes.
Harden logger initialization/imports and add focused tests for argv, boolean parsing, frontmatter, and CLI subcommands.
2026-01-18 23:10:39 +00:00

161 lines
5.4 KiB
TypeScript

import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
import { loadModelCatalog } from "../agents/model-catalog.js";
import {
getModelRefStatus,
resolveConfiguredModelRef,
resolveHooksGmailModel,
} from "../agents/model-selection.js";
import type { CliDeps } from "../cli/deps.js";
import type { loadConfig } from "../config/config.js";
import { isTruthyEnvValue } from "../infra/env.js";
import { startGmailWatcher } from "../hooks/gmail-watcher.js";
import {
clearInternalHooks,
createInternalHookEvent,
triggerInternalHook,
} from "../hooks/internal-hooks.js";
import { loadInternalHooks } from "../hooks/loader.js";
import type { loadClawdbotPlugins } from "../plugins/loader.js";
import { type PluginServicesHandle, startPluginServices } from "../plugins/services.js";
import { startBrowserControlServerIfEnabled } from "./server-browser.js";
import {
scheduleRestartSentinelWake,
shouldWakeFromRestartSentinel,
} from "./server-restart-sentinel.js";
export async function startGatewaySidecars(params: {
cfg: ReturnType<typeof loadConfig>;
pluginRegistry: ReturnType<typeof loadClawdbotPlugins>;
defaultWorkspaceDir: string;
deps: CliDeps;
startChannels: () => Promise<void>;
log: { warn: (msg: string) => void };
logHooks: {
info: (msg: string) => void;
warn: (msg: string) => void;
error: (msg: string) => void;
};
logChannels: { info: (msg: string) => void; error: (msg: string) => void };
logBrowser: { error: (msg: string) => void };
}) {
// Start clawd browser control server (unless disabled via config).
let browserControl: Awaited<ReturnType<typeof startBrowserControlServerIfEnabled>> = null;
try {
browserControl = await startBrowserControlServerIfEnabled();
} catch (err) {
params.logBrowser.error(`server failed to start: ${String(err)}`);
}
// Start Gmail watcher if configured (hooks.gmail.account).
if (!isTruthyEnvValue(process.env.CLAWDBOT_SKIP_GMAIL_WATCHER)) {
try {
const gmailResult = await startGmailWatcher(params.cfg);
if (gmailResult.started) {
params.logHooks.info("gmail watcher started");
} else if (
gmailResult.reason &&
gmailResult.reason !== "hooks not enabled" &&
gmailResult.reason !== "no gmail account configured"
) {
params.logHooks.warn(`gmail watcher not started: ${gmailResult.reason}`);
}
} catch (err) {
params.logHooks.error(`gmail watcher failed to start: ${String(err)}`);
}
}
// Validate hooks.gmail.model if configured.
if (params.cfg.hooks?.gmail?.model) {
const hooksModelRef = resolveHooksGmailModel({
cfg: params.cfg,
defaultProvider: DEFAULT_PROVIDER,
});
if (hooksModelRef) {
const { provider: defaultProvider, model: defaultModel } = resolveConfiguredModelRef({
cfg: params.cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const catalog = await loadModelCatalog({ config: params.cfg });
const status = getModelRefStatus({
cfg: params.cfg,
catalog,
ref: hooksModelRef,
defaultProvider,
defaultModel,
});
if (!status.allowed) {
params.logHooks.warn(
`hooks.gmail.model "${status.key}" not in agents.defaults.models allowlist (will use primary instead)`,
);
}
if (!status.inCatalog) {
params.logHooks.warn(
`hooks.gmail.model "${status.key}" not in the model catalog (may fail at runtime)`,
);
}
}
}
// Load internal hook handlers from configuration and directory discovery.
try {
// Clear any previously registered hooks to ensure fresh loading
clearInternalHooks();
const loadedCount = await loadInternalHooks(params.cfg, params.defaultWorkspaceDir);
if (loadedCount > 0) {
params.logHooks.info(
`loaded ${loadedCount} internal hook handler${loadedCount > 1 ? "s" : ""}`,
);
}
} catch (err) {
params.logHooks.error(`failed to load hooks: ${String(err)}`);
}
// Launch configured channels so gateway replies via the surface the message came from.
// Tests can opt out via CLAWDBOT_SKIP_CHANNELS (or legacy CLAWDBOT_SKIP_PROVIDERS).
const skipChannels =
isTruthyEnvValue(process.env.CLAWDBOT_SKIP_CHANNELS) ||
isTruthyEnvValue(process.env.CLAWDBOT_SKIP_PROVIDERS);
if (!skipChannels) {
try {
await params.startChannels();
} catch (err) {
params.logChannels.error(`channel startup failed: ${String(err)}`);
}
} else {
params.logChannels.info(
"skipping channel start (CLAWDBOT_SKIP_CHANNELS=1 or CLAWDBOT_SKIP_PROVIDERS=1)",
);
}
if (params.cfg.hooks?.internal?.enabled) {
setTimeout(() => {
const hookEvent = createInternalHookEvent("gateway", "startup", "gateway:startup", {
cfg: params.cfg,
deps: params.deps,
workspaceDir: params.defaultWorkspaceDir,
});
void triggerInternalHook(hookEvent);
}, 250);
}
let pluginServices: PluginServicesHandle | null = null;
try {
pluginServices = await startPluginServices({
registry: params.pluginRegistry,
config: params.cfg,
workspaceDir: params.defaultWorkspaceDir,
});
} catch (err) {
params.log.warn(`plugin services failed to start: ${String(err)}`);
}
if (shouldWakeFromRestartSentinel()) {
setTimeout(() => {
void scheduleRestartSentinelWake({ deps: params.deps });
}, 750);
}
return { browserControl, pluginServices };
}