diff --git a/README.md b/README.md index 12e620d9333..10d1b80d2ba 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,28 @@ openclaw --profile dench gateway restart openclaw --profile dench uninstall ``` +### Daemonless / Docker + +For containers or environments without systemd/launchd, set the environment variable once: + +```bash +export DENCHCLAW_DAEMONLESS=1 +``` + +This skips all gateway daemon management (install/start/stop/restart) and launchd LaunchAgent installation across all commands. You must start the gateway yourself as a foreground process: + +```bash +openclaw --profile dench gateway --port 19001 +``` + +Alternatively, pass `--skip-daemon-install` to individual commands: + +```bash +npx denchclaw --skip-daemon-install +npx denchclaw update --skip-daemon-install +npx denchclaw start --skip-daemon-install +``` + --- ## Development diff --git a/package.json b/package.json index 33dbf7ef8c5..2ebe6337f91 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "denchclaw", - "version": "2.3.4", + "version": "2.3.5", "description": "Fully Managed OpenClaw Framework for managing your CRM, Sales Automation and Outreach agents. The only local productivity tool you need.", "keywords": [], "homepage": "https://github.com/DenchHQ/DenchClaw#readme", diff --git a/packages/dench/package.json b/packages/dench/package.json index 038b96adbbc..e2e9282fe5e 100644 --- a/packages/dench/package.json +++ b/packages/dench/package.json @@ -1,6 +1,6 @@ { "name": "dench", - "version": "2.3.4", + "version": "2.3.5", "description": "Shorthand alias for denchclaw — AI-powered CRM platform CLI", "license": "MIT", "repository": { @@ -16,7 +16,7 @@ ], "type": "module", "dependencies": { - "denchclaw": "^2.3.4" + "denchclaw": "^2.3.5" }, "engines": { "node": ">=22.12.0" diff --git a/src/cli/bootstrap-external.ts b/src/cli/bootstrap-external.ts index 40b50a47518..744b8a907e2 100644 --- a/src/cli/bootstrap-external.ts +++ b/src/cli/bootstrap-external.ts @@ -13,6 +13,7 @@ import path from "node:path"; import process from "node:process"; import { confirm, isCancel, select, spinner, text } from "@clack/prompts"; import json5 from "json5"; +import { isDaemonlessMode } from "../config/paths.js"; import { isTruthyEnvValue } from "../infra/env.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { readTelemetryConfig, markNoticeShown } from "../telemetry/config.js"; @@ -95,6 +96,7 @@ export type BootstrapOptions = { denchCloudApiKey?: string; denchCloudModel?: string; denchGatewayUrl?: string; + skipDaemonInstall?: boolean; }; type BootstrapSummary = { @@ -2148,6 +2150,7 @@ export async function bootstrapCommand( runtime.log(theme.warn(appliedProfile.warning)); } + const daemonless = isDaemonlessMode(opts); const bootstrapStartTime = Date.now(); if (!opts.json) { @@ -2325,7 +2328,7 @@ export async function bootstrapCommand( "--profile", profile, "onboard", - "--install-daemon", + ...(daemonless ? [] : ["--install-daemon"]), "--gateway-bind", "loopback", "--gateway-port", @@ -2342,6 +2345,9 @@ export async function bootstrapCommand( } onboardArgv.push("--accept-risk", "--skip-ui"); + if (daemonless) { + onboardArgv.push("--skip-health"); + } if (nonInteractive) { await runOpenClawOrThrow({ @@ -2410,55 +2416,62 @@ export async function bootstrapCommand( postOnboardSpinner?.message("Configuring subagent defaults…"); await ensureSubagentDefaults(openclawCommand, profile); - // ── Single post-config gateway restart ── - // All Dench-owned config has been applied. Restart the gateway once so the - // daemon picks up plugin, model, and subagent changes that were written after - // onboard started it. No helper above triggers its own restart. - postOnboardSpinner?.message("Restarting gateway…"); - try { - await runOpenClawOrThrow({ - openclawCommand, - args: ["--profile", profile, "gateway", "restart"], - timeoutMs: 60_000, - errorMessage: "Failed to restart gateway after config update.", - }); - } catch { - // Gateway may not be running (e.g. onboard daemon install failed on this - // platform). The final readiness check below will catch this. - } - - // ── Final readiness verification ── - // Give the gateway time to finish starting after the restart, then verify - // readiness. The probe retries here replace the old pattern of probing - // immediately (which raced gateway startup) and jumping straight into a - // destructive stop/install/start auto-fix cycle. - postOnboardSpinner?.message("Waiting for gateway…"); - let gatewayProbe = await probeGateway(openclawCommand, profile, gatewayPort); - for (let attempt = 0; attempt < 4 && !gatewayProbe.ok; attempt += 1) { - await sleep(750); - postOnboardSpinner?.message(`Probing gateway health (attempt ${attempt + 2}/5)…`); - gatewayProbe = await probeGateway(openclawCommand, profile, gatewayPort); - } - - // Repair is failure-only: only invoked when the retried final verification - // still reports the gateway as unreachable. + // ── Gateway daemon restart + readiness verification ── + // Skipped entirely in daemonless mode — the user manages the gateway process + // externally (e.g. `openclaw gateway --port ` as a foreground process). + let gatewayProbe: { ok: boolean; detail?: string }; let gatewayAutoFix: GatewayAutoFixResult | undefined; - if (!gatewayProbe.ok) { - postOnboardSpinner?.message("Gateway unreachable, attempting auto-fix…"); - gatewayAutoFix = await attemptGatewayAutoFix({ - openclawCommand, - profile, - stateDir, - gatewayPort, - }); - gatewayProbe = gatewayAutoFix.finalProbe; - if (!gatewayProbe.ok && gatewayAutoFix.failureSummary) { - gatewayProbe = { - ...gatewayProbe, - detail: [gatewayProbe.detail, gatewayAutoFix.failureSummary] - .filter((value, index, self) => value && self.indexOf(value) === index) - .join(" | "), - }; + + if (daemonless) { + gatewayProbe = { ok: true, detail: "skipped (daemonless)" }; + } else { + // All Dench-owned config has been applied. Restart the gateway once so the + // daemon picks up plugin, model, and subagent changes that were written after + // onboard started it. No helper above triggers its own restart. + postOnboardSpinner?.message("Restarting gateway…"); + try { + await runOpenClawOrThrow({ + openclawCommand, + args: ["--profile", profile, "gateway", "restart"], + timeoutMs: 60_000, + errorMessage: "Failed to restart gateway after config update.", + }); + } catch { + // Gateway may not be running (e.g. onboard daemon install failed on this + // platform). The final readiness check below will catch this. + } + + // Give the gateway time to finish starting after the restart, then verify + // readiness. The probe retries here replace the old pattern of probing + // immediately (which raced gateway startup) and jumping straight into a + // destructive stop/install/start auto-fix cycle. + postOnboardSpinner?.message("Waiting for gateway…"); + gatewayProbe = await probeGateway(openclawCommand, profile, gatewayPort); + for (let attempt = 0; attempt < 4 && !gatewayProbe.ok; attempt += 1) { + await sleep(750); + postOnboardSpinner?.message(`Probing gateway health (attempt ${attempt + 2}/5)…`); + gatewayProbe = await probeGateway(openclawCommand, profile, gatewayPort); + } + + // Repair is failure-only: only invoked when the retried final verification + // still reports the gateway as unreachable. + if (!gatewayProbe.ok) { + postOnboardSpinner?.message("Gateway unreachable, attempting auto-fix…"); + gatewayAutoFix = await attemptGatewayAutoFix({ + openclawCommand, + profile, + stateDir, + gatewayPort, + }); + gatewayProbe = gatewayAutoFix.finalProbe; + if (!gatewayProbe.ok && gatewayAutoFix.failureSummary) { + gatewayProbe = { + ...gatewayProbe, + detail: [gatewayProbe.detail, gatewayAutoFix.failureSummary] + .filter((value, index, self) => value && self.indexOf(value) === index) + .join(" | "), + }; + } } } const gatewayUrl = `ws://127.0.0.1:${gatewayPort}`; diff --git a/src/cli/program/register.bootstrap.ts b/src/cli/program/register.bootstrap.ts index 282015a3fd9..46aa52a636a 100644 --- a/src/cli/program/register.bootstrap.ts +++ b/src/cli/program/register.bootstrap.ts @@ -21,6 +21,7 @@ export function registerBootstrapCommand(program: Command) { .option("--dench-cloud-api-key ", "Dench Cloud API key for bootstrap-driven setup") .option("--dench-cloud-model ", "Stable or public Dench Cloud model id to use as default") .option("--dench-gateway-url ", "Override the Dench Cloud gateway base URL") + .option("--skip-daemon-install", "Skip gateway daemon/service installation (for containers or environments without systemd/launchd)", false) .option("--no-open", "Do not open the browser automatically") .option("--json", "Output summary as JSON", false) .addHelpText( @@ -43,6 +44,7 @@ export function registerBootstrapCommand(program: Command) { denchCloudApiKey: opts.denchCloudApiKey as string | undefined, denchCloudModel: opts.denchCloudModel as string | undefined, denchGatewayUrl: opts.denchGatewayUrl as string | undefined, + skipDaemonInstall: Boolean(opts.skipDaemonInstall), noOpen: Boolean(opts.open === false), json: Boolean(opts.json), }); diff --git a/src/cli/program/register.restart.ts b/src/cli/program/register.restart.ts index 7dbc6eb9fd4..867cec8ffa4 100644 --- a/src/cli/program/register.restart.ts +++ b/src/cli/program/register.restart.ts @@ -10,6 +10,7 @@ export function registerRestartCommand(program: Command) { .option("--profile ", "Compatibility flag; non-dench values are ignored with a warning") .option("--web-port ", "Web runtime port override") .option("--no-open", "Do not open the browser automatically") + .option("--skip-daemon-install", "Skip gateway daemon/service management (for containers or environments without systemd/launchd)", false) .option("--json", "Output summary as JSON", false) .action(async (opts) => { await runCommandWithRuntime(defaultRuntime, async () => { @@ -17,6 +18,7 @@ export function registerRestartCommand(program: Command) { profile: opts.profile as string | undefined, webPort: opts.webPort as string | undefined, noOpen: Boolean(opts.open === false), + skipDaemonInstall: Boolean(opts.skipDaemonInstall), json: Boolean(opts.json), }); }); diff --git a/src/cli/program/register.start.ts b/src/cli/program/register.start.ts index 2a10d785a90..0c91b3af8b9 100644 --- a/src/cli/program/register.start.ts +++ b/src/cli/program/register.start.ts @@ -10,6 +10,7 @@ export function registerStartCommand(program: Command) { .option("--profile ", "Compatibility flag; non-dench values are ignored with a warning") .option("--web-port ", "Web runtime port override") .option("--no-open", "Do not open the browser automatically") + .option("--skip-daemon-install", "Skip gateway daemon/service management (for containers or environments without systemd/launchd)", false) .option("--json", "Output summary as JSON", false) .action(async (opts) => { await runCommandWithRuntime(defaultRuntime, async () => { @@ -17,6 +18,7 @@ export function registerStartCommand(program: Command) { profile: opts.profile as string | undefined, webPort: opts.webPort as string | undefined, noOpen: Boolean(opts.open === false), + skipDaemonInstall: Boolean(opts.skipDaemonInstall), json: Boolean(opts.json), }); }); diff --git a/src/cli/program/register.stop.ts b/src/cli/program/register.stop.ts index c2474879558..bb6594600e9 100644 --- a/src/cli/program/register.stop.ts +++ b/src/cli/program/register.stop.ts @@ -9,12 +9,14 @@ export function registerStopCommand(program: Command) { .description("Stop Dench managed web runtime on the configured port") .option("--profile ", "Compatibility flag; non-dench values are ignored with a warning") .option("--web-port ", "Web runtime port override") + .option("--skip-daemon-install", "Skip gateway daemon/service management (for containers or environments without systemd/launchd)", false) .option("--json", "Output summary as JSON", false) .action(async (opts) => { await runCommandWithRuntime(defaultRuntime, async () => { await stopWebRuntimeCommand({ profile: opts.profile as string | undefined, webPort: opts.webPort as string | undefined, + skipDaemonInstall: Boolean(opts.skipDaemonInstall), json: Boolean(opts.json), }); }); diff --git a/src/cli/program/register.update.ts b/src/cli/program/register.update.ts index 264171e7fa2..ce0060db057 100644 --- a/src/cli/program/register.update.ts +++ b/src/cli/program/register.update.ts @@ -12,6 +12,7 @@ export function registerUpdateCommand(program: Command) { .option("--non-interactive", "Fail instead of prompting for major-gate approval", false) .option("--yes", "Approve mandatory major-gate OpenClaw update", false) .option("--no-open", "Do not open the browser automatically") + .option("--skip-daemon-install", "Skip gateway daemon/service management (for containers or environments without systemd/launchd)", false) .option("--json", "Output summary as JSON", false) .action(async (opts) => { await runCommandWithRuntime(defaultRuntime, async () => { @@ -21,6 +22,7 @@ export function registerUpdateCommand(program: Command) { nonInteractive: Boolean(opts.nonInteractive), yes: Boolean(opts.yes), noOpen: Boolean(opts.open === false), + skipDaemonInstall: Boolean(opts.skipDaemonInstall), json: Boolean(opts.json), }); }); diff --git a/src/cli/web-runtime-command.test.ts b/src/cli/web-runtime-command.test.ts index dc5010ef925..239a7f0dc52 100644 --- a/src/cli/web-runtime-command.test.ts +++ b/src/cli/web-runtime-command.test.ts @@ -273,6 +273,22 @@ describe("updateWebRuntimeCommand", () => { }); }); + it("skips gateway daemon restart and LaunchAgent in daemonless mode", async () => { + webRuntimeMocks.runOpenClawCommand.mockClear(); + launchdMocks.uninstallWebRuntimeLaunchAgent.mockClear(); + const runtime = runtimeStub(); + const summary = await updateWebRuntimeCommand( + { nonInteractive: true, skipDaemonInstall: true }, + runtime, + ); + + expect(webRuntimeMocks.runOpenClawCommand).not.toHaveBeenCalled(); + expect(launchdMocks.uninstallWebRuntimeLaunchAgent).not.toHaveBeenCalled(); + expect(summary.gatewayRestarted).toBe(false); + expect(summary.gatewayError).toBeUndefined(); + expect(summary.ready).toBe(true); + }); + it("skips OpenClaw update on minor upgrades while still refreshing runtime (avoids unnecessary blocking)", async () => { webRuntimeMocks.evaluateMajorVersionTransition.mockReturnValue({ previousMajor: 2, @@ -410,6 +426,30 @@ describe("startWebRuntimeCommand", () => { expect(summary.started).toBe(true); }); + it("skips gateway daemon restart and LaunchAgent in daemonless mode, uses child process", async () => { + webRuntimeMocks.runOpenClawCommand.mockClear(); + launchdMocks.installWebRuntimeLaunchAgent.mockClear(); + launchdMocks.uninstallWebRuntimeLaunchAgent.mockClear(); + webRuntimeMocks.startManagedWebRuntime.mockClear(); + const runtime = runtimeStub(); + const summary = await startWebRuntimeCommand( + { webPort: "3100", skipDaemonInstall: true }, + runtime, + ); + + expect(webRuntimeMocks.runOpenClawCommand).not.toHaveBeenCalled(); + expect(launchdMocks.installWebRuntimeLaunchAgent).not.toHaveBeenCalled(); + expect(launchdMocks.uninstallWebRuntimeLaunchAgent).not.toHaveBeenCalled(); + expect(webRuntimeMocks.startManagedWebRuntime).toHaveBeenCalledWith({ + stateDir: "/tmp/.openclaw-dench", + port: 3100, + gatewayPort: 19001, + }); + expect(summary.started).toBe(true); + expect(summary.gatewayRestarted).toBe(false); + expect(summary.gatewayError).toBeUndefined(); + }); + it("falls back to DenchClaw port 19001 when manifest has no lastGatewayPort (prevents 18789 hijack)", async () => { webRuntimeMocks.readManagedWebRuntimeManifest.mockReturnValue({ schemaVersion: 1, diff --git a/src/cli/web-runtime-command.ts b/src/cli/web-runtime-command.ts index ecd7ebba1c7..09ca6a58056 100644 --- a/src/cli/web-runtime-command.ts +++ b/src/cli/web-runtime-command.ts @@ -6,7 +6,7 @@ import { confirm, isCancel, spinner } from "@clack/prompts"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { stylePromptMessage } from "../terminal/prompt-style.js"; import { theme } from "../terminal/theme.js"; -import { DENCHCLAW_DEFAULT_GATEWAY_PORT } from "../config/paths.js"; +import { DENCHCLAW_DEFAULT_GATEWAY_PORT, isDaemonlessMode } from "../config/paths.js"; import { VERSION } from "../version.js"; import { applyCliProfileEnv } from "./profile.js"; import { @@ -43,12 +43,14 @@ export type UpdateWebRuntimeOptions = { yes?: boolean; noOpen?: boolean; json?: boolean; + skipDaemonInstall?: boolean; }; export type StopWebRuntimeOptions = { profile?: string; webPort?: string | number; json?: boolean; + skipDaemonInstall?: boolean; }; export type StartWebRuntimeOptions = { @@ -56,6 +58,7 @@ export type StartWebRuntimeOptions = { webPort?: string | number; noOpen?: boolean; json?: boolean; + skipDaemonInstall?: boolean; }; export type UpdateWebRuntimeSummary = { @@ -397,6 +400,7 @@ export async function updateWebRuntimeCommand( await runOpenClawUpdateWithProgress(openclawCommand); } + const daemonless = isDaemonlessMode(opts); const selectedPort = parseOptionalPort(opts.webPort) ?? parseOptionalPort(previousManifest?.lastPort) ?? @@ -404,7 +408,7 @@ export async function updateWebRuntimeCommand( DEFAULT_WEB_APP_PORT; const gatewayPort = resolveGatewayPort(stateDir); - if (process.platform === "darwin") { + if (!daemonless && process.platform === "darwin") { uninstallWebRuntimeLaunchAgent(); } @@ -414,11 +418,9 @@ export async function updateWebRuntimeCommand( includeLegacyStandalone: true, }); - const gatewayResult = await restartGatewayDaemon({ - profile, - gatewayPort, - json: Boolean(opts.json), - }); + const gatewayResult: { restarted: boolean; error?: string } = daemonless + ? { restarted: false, error: "skipped (daemonless)" } + : await restartGatewayDaemon({ profile, gatewayPort, json: Boolean(opts.json) }); const workspaceDirs = discoverWorkspaceDirs(stateDir); const skillSyncResult = syncManagedSkills({ workspaceDirs, packageRoot }); @@ -430,7 +432,7 @@ export async function updateWebRuntimeCommand( port: selectedPort, gatewayPort, startFn: - process.platform === "darwin" + !daemonless && process.platform === "darwin" ? (p) => installWebRuntimeLaunchAgent(p) : undefined, }); @@ -449,7 +451,7 @@ export async function updateWebRuntimeCommand( ready: ensureResult.ready, reason: ensureResult.reason, gatewayRestarted: gatewayResult.restarted, - gatewayError: gatewayResult.error, + gatewayError: daemonless ? undefined : gatewayResult.error, skillSync: skillSyncResult, }; @@ -459,7 +461,11 @@ export async function updateWebRuntimeCommand( runtime.log(`Profile: ${profile}`); runtime.log(`Version: ${VERSION}`); runtime.log(`Web port: ${selectedPort}`); - runtime.log(`Gateway: ${summary.gatewayRestarted ? "restarted" : "restart failed"}`); + if (daemonless) { + runtime.log(`Gateway: skipped (daemonless mode)`); + } else { + runtime.log(`Gateway: ${summary.gatewayRestarted ? "restarted" : "restart failed"}`); + } if (summary.gatewayError) { runtime.log(theme.warn(`Gateway error: ${summary.gatewayError}`)); } @@ -508,7 +514,7 @@ export async function stopWebRuntimeCommand( const stateDir = resolveProfileStateDir(profile); const selectedPort = parseOptionalPort(opts.webPort) ?? readLastKnownWebPort(stateDir); - if (process.platform === "darwin") { + if (!isDaemonlessMode(opts) && process.platform === "darwin") { uninstallWebRuntimeLaunchAgent(); } @@ -570,11 +576,12 @@ export async function startWebRuntimeCommand( runtime.log(theme.warn(appliedProfile.warning)); } + const daemonless = isDaemonlessMode(opts); const stateDir = resolveProfileStateDir(profile); const selectedPort = parseOptionalPort(opts.webPort) ?? readLastKnownWebPort(stateDir); const gatewayPort = resolveGatewayPort(stateDir); - if (process.platform === "darwin") { + if (!daemonless && process.platform === "darwin") { uninstallWebRuntimeLaunchAgent(); } @@ -590,14 +597,12 @@ export async function startWebRuntimeCommand( ); } - const gatewayResult = await restartGatewayDaemon({ - profile, - gatewayPort, - json: Boolean(opts.json), - }); + const gatewayResult: { restarted: boolean; error?: string } = daemonless + ? { restarted: false, error: "skipped (daemonless)" } + : await restartGatewayDaemon({ profile, gatewayPort, json: Boolean(opts.json) }); let startResult; - if (process.platform === "darwin") { + if (!daemonless && process.platform === "darwin") { startResult = installWebRuntimeLaunchAgent({ stateDir, port: selectedPort, gatewayPort }); if (!startResult.started && startResult.reason !== "runtime-missing") { startResult = startManagedWebRuntime({ stateDir, port: selectedPort, gatewayPort }); @@ -625,7 +630,7 @@ export async function startWebRuntimeCommand( started: probe.ok, reason: probe.reason, gatewayRestarted: gatewayResult.restarted, - gatewayError: gatewayResult.error, + gatewayError: daemonless ? undefined : gatewayResult.error, }; if (opts.json) { @@ -637,7 +642,11 @@ export async function startWebRuntimeCommand( runtime.log(theme.heading(`Dench web ${label}`)); runtime.log(`Profile: ${profile}`); runtime.log(`Web port: ${selectedPort}`); - runtime.log(`Gateway: ${summary.gatewayRestarted ? "restarted" : "restart failed"}`); + if (daemonless) { + runtime.log(`Gateway: skipped (daemonless mode)`); + } else { + runtime.log(`Gateway: ${summary.gatewayRestarted ? "restarted" : "restart failed"}`); + } if (summary.gatewayError) { runtime.log(theme.warn(`Gateway error: ${summary.gatewayError}`)); } diff --git a/src/config/paths.test.ts b/src/config/paths.test.ts index ada552fea43..59e4276f2cc 100644 --- a/src/config/paths.test.ts +++ b/src/config/paths.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from "vitest"; import { + isDaemonlessMode, resolveGatewayPort, DEFAULT_GATEWAY_PORT, DENCHCLAW_DEFAULT_GATEWAY_PORT, @@ -85,6 +86,32 @@ describe("resolveGatewayPort", () => { }); }); +describe("isDaemonlessMode", () => { + it("returns false when no opt and no env var", () => { + expect(isDaemonlessMode(undefined, {})).toBe(false); + expect(isDaemonlessMode({}, {})).toBe(false); + }); + + it("returns true when opts.skipDaemonInstall is set", () => { + expect(isDaemonlessMode({ skipDaemonInstall: true }, {})).toBe(true); + }); + + it("returns true when DENCHCLAW_DAEMONLESS=1 env var is set", () => { + expect(isDaemonlessMode(undefined, { DENCHCLAW_DAEMONLESS: "1" })).toBe(true); + expect(isDaemonlessMode({}, { DENCHCLAW_DAEMONLESS: "1" })).toBe(true); + }); + + it("returns false for non-'1' env values", () => { + expect(isDaemonlessMode(undefined, { DENCHCLAW_DAEMONLESS: "true" })).toBe(false); + expect(isDaemonlessMode(undefined, { DENCHCLAW_DAEMONLESS: "yes" })).toBe(false); + expect(isDaemonlessMode(undefined, { DENCHCLAW_DAEMONLESS: "0" })).toBe(false); + }); + + it("opts.skipDaemonInstall=true wins even without env var", () => { + expect(isDaemonlessMode({ skipDaemonInstall: true }, { DENCHCLAW_DAEMONLESS: "0" })).toBe(true); + }); +}); + describe("port constants", () => { it("DenchClaw default port is distinct from OpenClaw default (prevents port collision)", () => { expect(DENCHCLAW_DEFAULT_GATEWAY_PORT).not.toBe(DEFAULT_GATEWAY_PORT); diff --git a/src/config/paths.ts b/src/config/paths.ts index d3c4e442a9f..5f4cb858164 100644 --- a/src/config/paths.ts +++ b/src/config/paths.ts @@ -22,6 +22,13 @@ export function resolveIsNixMode(env: NodeJS.ProcessEnv = process.env): boolean export const isNixMode = resolveIsNixMode(); +export function isDaemonlessMode( + opts?: { skipDaemonInstall?: boolean }, + env: NodeJS.ProcessEnv = process.env, +): boolean { + return Boolean(opts?.skipDaemonInstall) || env.DENCHCLAW_DAEMONLESS === "1"; +} + // Support historical (and occasionally misspelled) legacy state dirs. const LEGACY_STATE_DIRNAMES = [".clawdbot", ".moldbot", ".moltbot"] as const; const NEW_STATE_DIRNAME = ".openclaw-dench";