fix(onboarding): daemon progress + web search setup

This commit is contained in:
Peter Steinberger
2026-01-15 08:28:37 +00:00
parent f1ac18933c
commit a39bb4310c
3 changed files with 173 additions and 93 deletions

View File

@@ -8,6 +8,7 @@ import {
} from "../daemon/runtime-paths.js"; } from "../daemon/runtime-paths.js";
import { resolveGatewayService } from "../daemon/service.js"; import { resolveGatewayService } from "../daemon/service.js";
import { buildServiceEnvironment } from "../daemon/service-env.js"; import { buildServiceEnvironment } from "../daemon/service-env.js";
import { withProgress } from "../cli/progress.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
import { note } from "../terminal/note.js"; import { note } from "../terminal/note.js";
import { confirm, select } from "./configure.shared.js"; import { confirm, select } from "./configure.shared.js";
@@ -43,63 +44,89 @@ export async function maybeInstallDaemon(params: {
params.runtime, params.runtime,
); );
if (action === "restart") { if (action === "restart") {
await service.restart({ await withProgress(
profile: process.env.CLAWDBOT_PROFILE, { label: "Gateway daemon", indeterminate: true, delayMs: 0 },
stdout: process.stdout, async (progress) => {
}); progress.setLabel("Restarting Gateway daemon…");
await service.restart({
profile: process.env.CLAWDBOT_PROFILE,
stdout: process.stdout,
});
progress.setLabel("Gateway daemon restarted.");
},
);
shouldCheckLinger = true; shouldCheckLinger = true;
shouldInstall = false; shouldInstall = false;
} }
if (action === "skip") return; if (action === "skip") return;
if (action === "reinstall") { if (action === "reinstall") {
await service.uninstall({ env: process.env, stdout: process.stdout }); await withProgress(
{ label: "Gateway daemon", indeterminate: true, delayMs: 0 },
async (progress) => {
progress.setLabel("Uninstalling Gateway daemon…");
await service.uninstall({ env: process.env, stdout: process.stdout });
progress.setLabel("Gateway daemon uninstalled.");
},
);
} }
} }
if (shouldInstall) { if (shouldInstall) {
if (!params.daemonRuntime) { await withProgress(
daemonRuntime = guardCancel( { label: "Gateway daemon", indeterminate: true, delayMs: 0 },
await select({ async (progress) => {
message: "Gateway daemon runtime", if (!params.daemonRuntime) {
options: GATEWAY_DAEMON_RUNTIME_OPTIONS, daemonRuntime = guardCancel(
initialValue: DEFAULT_GATEWAY_DAEMON_RUNTIME, await select({
}), message: "Gateway daemon runtime",
params.runtime, options: GATEWAY_DAEMON_RUNTIME_OPTIONS,
) as GatewayDaemonRuntime; initialValue: DEFAULT_GATEWAY_DAEMON_RUNTIME,
} }),
const devMode = params.runtime,
process.argv[1]?.includes(`${path.sep}src${path.sep}`) && process.argv[1]?.endsWith(".ts"); ) as GatewayDaemonRuntime;
const nodePath = await resolvePreferredNodePath({ }
env: process.env,
runtime: daemonRuntime, progress.setLabel("Preparing Gateway daemon…");
});
const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({ const devMode =
port: params.port, process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
dev: devMode, process.argv[1]?.endsWith(".ts");
runtime: daemonRuntime, const nodePath = await resolvePreferredNodePath({
nodePath, env: process.env,
}); runtime: daemonRuntime,
if (daemonRuntime === "node") { });
const systemNode = await resolveSystemNodeInfo({ env: process.env }); const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({
const warning = renderSystemNodeWarning(systemNode, programArguments[0]); port: params.port,
if (warning) note(warning, "Gateway runtime"); dev: devMode,
} runtime: daemonRuntime,
const environment = buildServiceEnvironment({ nodePath,
env: process.env, });
port: params.port, if (daemonRuntime === "node") {
token: params.gatewayToken, const systemNode = await resolveSystemNodeInfo({ env: process.env });
launchdLabel: const warning = renderSystemNodeWarning(systemNode, programArguments[0]);
process.platform === "darwin" if (warning) note(warning, "Gateway runtime");
? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE) }
: undefined, const environment = buildServiceEnvironment({
}); env: process.env,
await service.install({ port: params.port,
env: process.env, token: params.gatewayToken,
stdout: process.stdout, launchdLabel:
programArguments, process.platform === "darwin"
workingDirectory, ? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE)
environment, : undefined,
}); });
progress.setLabel("Installing Gateway daemon…");
await service.install({
env: process.env,
stdout: process.stdout,
programArguments,
workingDirectory,
environment,
});
progress.setLabel("Gateway daemon installed.");
},
);
shouldCheckLinger = true; shouldCheckLinger = true;
} }

View File

@@ -98,9 +98,18 @@ async function promptWebToolsConfig(
const existingFetch = nextConfig.tools?.web?.fetch; const existingFetch = nextConfig.tools?.web?.fetch;
const hasSearchKey = Boolean(existingSearch?.apiKey); const hasSearchKey = Boolean(existingSearch?.apiKey);
note(
[
"Web search lets your agent look things up online using the `web_search` tool.",
"It requires a Brave Search API key (you can store it in the config or set BRAVE_API_KEY in the Gateway environment).",
"Docs: https://docs.clawd.bot/tools/web",
].join("\n"),
"Web search",
);
const enableSearch = guardCancel( const enableSearch = guardCancel(
await confirm({ await confirm({
message: "Enable web_search (Brave Search API)?", message: "Enable web_search (Brave Search)?",
initialValue: existingSearch?.enabled ?? hasSearchKey, initialValue: existingSearch?.enabled ?? hasSearchKey,
}), }),
runtime, runtime,
@@ -116,7 +125,7 @@ async function promptWebToolsConfig(
await text({ await text({
message: hasSearchKey message: hasSearchKey
? "Brave Search API key (leave blank to keep current or use BRAVE_API_KEY)" ? "Brave Search API key (leave blank to keep current or use BRAVE_API_KEY)"
: "Brave Search API key (leave blank to use BRAVE_API_KEY)", : "Brave Search API key (paste it here; leave blank to use BRAVE_API_KEY)",
placeholder: hasSearchKey ? "Leave blank to keep current" : "BSA...", placeholder: hasSearchKey ? "Leave blank to keep current" : "BSA...",
}), }),
runtime, runtime,
@@ -127,7 +136,8 @@ async function promptWebToolsConfig(
} else if (!hasSearchKey) { } else if (!hasSearchKey) {
note( note(
[ [
"No key stored. web_search needs BRAVE_API_KEY or tools.web.search.apiKey.", "No key stored yet, so web_search will stay unavailable.",
"Store a key here or set BRAVE_API_KEY in the Gateway environment.",
"Docs: https://docs.clawd.bot/tools/web", "Docs: https://docs.clawd.bot/tools/web",
].join("\n"), ].join("\n"),
"Web search", "Web search",

View File

@@ -49,6 +49,19 @@ type FinalizeOnboardingOptions = {
export async function finalizeOnboardingWizard(options: FinalizeOnboardingOptions) { export async function finalizeOnboardingWizard(options: FinalizeOnboardingOptions) {
const { flow, opts, baseConfig, nextConfig, settings, prompter, runtime } = options; const { flow, opts, baseConfig, nextConfig, settings, prompter, runtime } = options;
const withWizardProgress = async <T>(
label: string,
options: { doneMessage?: string },
work: (progress: { update: (message: string) => void }) => Promise<T>,
): Promise<T> => {
const progress = prompter.progress(label);
try {
return await work(progress);
} finally {
progress.stop(options.doneMessage);
}
};
const systemdAvailable = const systemdAvailable =
process.platform === "linux" ? await isSystemdUserServiceAvailable() : true; process.platform === "linux" ? await isSystemdUserServiceAvailable() : true;
if (process.platform === "linux" && !systemdAvailable) { if (process.platform === "linux" && !systemdAvailable) {
@@ -123,12 +136,26 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
], ],
})) as "restart" | "reinstall" | "skip"; })) as "restart" | "reinstall" | "skip";
if (action === "restart") { if (action === "restart") {
await service.restart({ await withWizardProgress(
profile: process.env.CLAWDBOT_PROFILE, "Gateway daemon",
stdout: process.stdout, { doneMessage: "Gateway daemon restarted." },
}); async (progress) => {
progress.update("Restarting Gateway daemon…");
await service.restart({
profile: process.env.CLAWDBOT_PROFILE,
stdout: process.stdout,
});
},
);
} else if (action === "reinstall") { } else if (action === "reinstall") {
await service.uninstall({ env: process.env, stdout: process.stdout }); await withWizardProgress(
"Gateway daemon",
{ doneMessage: "Gateway daemon uninstalled." },
async (progress) => {
progress.update("Uninstalling Gateway daemon…");
await service.uninstall({ env: process.env, stdout: process.stdout });
},
);
} }
} }
@@ -138,37 +165,46 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
) { ) {
const devMode = const devMode =
process.argv[1]?.includes(`${path.sep}src${path.sep}`) && process.argv[1]?.endsWith(".ts"); process.argv[1]?.includes(`${path.sep}src${path.sep}`) && process.argv[1]?.endsWith(".ts");
const nodePath = await resolvePreferredNodePath({ await withWizardProgress(
env: process.env, "Gateway daemon",
runtime: daemonRuntime, { doneMessage: "Gateway daemon installed." },
}); async (progress) => {
const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({ progress.update("Preparing Gateway daemon…");
port: settings.port, const nodePath = await resolvePreferredNodePath({
dev: devMode, env: process.env,
runtime: daemonRuntime, runtime: daemonRuntime,
nodePath, });
}); const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({
if (daemonRuntime === "node") { port: settings.port,
const systemNode = await resolveSystemNodeInfo({ env: process.env }); dev: devMode,
const warning = renderSystemNodeWarning(systemNode, programArguments[0]); runtime: daemonRuntime,
if (warning) await prompter.note(warning, "Gateway runtime"); nodePath,
} });
const environment = buildServiceEnvironment({ if (daemonRuntime === "node") {
env: process.env, const systemNode = await resolveSystemNodeInfo({ env: process.env });
port: settings.port, const warning = renderSystemNodeWarning(systemNode, programArguments[0]);
token: settings.gatewayToken, if (warning) await prompter.note(warning, "Gateway runtime");
launchdLabel: }
process.platform === "darwin" const environment = buildServiceEnvironment({
? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE) env: process.env,
: undefined, port: settings.port,
}); token: settings.gatewayToken,
await service.install({ launchdLabel:
env: process.env, process.platform === "darwin"
stdout: process.stdout, ? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE)
programArguments, : undefined,
workingDirectory, });
environment,
}); progress.update("Installing Gateway daemon…");
await service.install({
env: process.env,
stdout: process.stdout,
programArguments,
workingDirectory,
environment,
});
},
);
} }
} }
@@ -353,16 +389,23 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
await prompter.note( await prompter.note(
hasWebSearchKey hasWebSearchKey
? [ ? [
"Web search is ready.", "Web search is enabled, so your agent can look things up online when needed.",
"",
webSearchKey webSearchKey
? "Brave API key: stored in config (tools.web.search.apiKey)." ? "API key: stored in config (tools.web.search.apiKey)."
: "Brave API key: provided via BRAVE_API_KEY env var.", : "API key: provided via BRAVE_API_KEY env var (Gateway environment).",
"Docs: https://docs.clawd.bot/tools/web", "Docs: https://docs.clawd.bot/tools/web",
].join("\n") ].join("\n")
: [ : [
"Recommended: set up a Brave Search API key for web_search.", "If you want your agent to be able to search the web, youll need an API key.",
"Easiest: clawdbot configure --section web (stores tools.web.search.apiKey).", "",
"Env alternative: BRAVE_API_KEY (gateway environment).", "Clawdbot uses Brave Search for the `web_search` tool. Without a Brave Search API key, web search wont work.",
"",
"Set it up interactively:",
"- Run: clawdbot configure --section web",
"- Enable web_search and paste your Brave Search API key",
"",
"Alternative: set BRAVE_API_KEY in the Gateway environment (no config changes).",
"Docs: https://docs.clawd.bot/tools/web", "Docs: https://docs.clawd.bot/tools/web",
].join("\n"), ].join("\n"),
"Web search (optional)", "Web search (optional)",