diff --git a/src/auto-reply/reply/commands.test.ts b/src/auto-reply/reply/commands.test.ts new file mode 100644 index 000000000..d17e7f25c --- /dev/null +++ b/src/auto-reply/reply/commands.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from "vitest"; + +import type { ClawdbotConfig } from "../../config/config.js"; +import type { MsgContext } from "../templating.js"; +import { buildCommandContext, handleCommands } from "./commands.js"; +import { parseInlineDirectives } from "./directive-handling.js"; + +function buildParams(commandBody: string, cfg: ClawdbotConfig) { + const ctx = { + Body: commandBody, + CommandBody: commandBody, + CommandSource: "text", + CommandAuthorized: true, + Provider: "whatsapp", + Surface: "whatsapp", + } as MsgContext; + + const command = buildCommandContext({ + ctx, + cfg, + isGroup: false, + triggerBodyNormalized: commandBody.trim().toLowerCase(), + commandAuthorized: true, + }); + + return { + ctx, + cfg, + command, + directives: parseInlineDirectives(commandBody), + sessionKey: "agent:main:main", + workspaceDir: "/tmp", + defaultGroupActivation: () => "mention", + resolvedVerboseLevel: "off" as const, + resolvedReasoningLevel: "off" as const, + resolveDefaultThinkingLevel: async () => undefined, + provider: "whatsapp", + model: "test-model", + contextTokens: 0, + isGroup: false, + }; +} + +describe("handleCommands gating", () => { + it("blocks /config when disabled", async () => { + const cfg = { + commands: { config: false, debug: false, text: true }, + whatsapp: { allowFrom: ["*"] }, + } as ClawdbotConfig; + const params = buildParams("/config show", cfg); + const result = await handleCommands(params); + expect(result.shouldContinue).toBe(false); + expect(result.reply?.text).toContain("/config is disabled"); + }); + + it("blocks /debug when disabled", async () => { + const cfg = { + commands: { config: false, debug: false, text: true }, + whatsapp: { allowFrom: ["*"] }, + } as ClawdbotConfig; + const params = buildParams("/debug show", cfg); + const result = await handleCommands(params); + expect(result.shouldContinue).toBe(false); + expect(result.reply?.text).toContain("/debug is disabled"); + }); +}); diff --git a/src/auto-reply/reply/commands.ts b/src/auto-reply/reply/commands.ts index cd93458fe..f0f5c6292 100644 --- a/src/auto-reply/reply/commands.ts +++ b/src/auto-reply/reply/commands.ts @@ -613,7 +613,10 @@ export async function handleCommands(params: { ); return { shouldContinue: false }; } - return { shouldContinue: false, reply: { text: buildCommandsMessage(cfg) } }; + return { + shouldContinue: false, + reply: { text: buildCommandsMessage(cfg) }, + }; } const statusRequested = diff --git a/src/cli/daemon-cli.ts b/src/cli/daemon-cli.ts index 6e09a6a24..766af3027 100644 --- a/src/cli/daemon-cli.ts +++ b/src/cli/daemon-cli.ts @@ -336,10 +336,7 @@ function renderGatewayServiceStartHints( } case "linux": { const unit = resolveGatewaySystemdServiceName(profile); - return [ - ...base, - `systemctl --user start ${unit}.service`, - ]; + return [...base, `systemctl --user start ${unit}.service`]; } case "win32": { const task = resolveGatewayWindowsTaskName(profile); @@ -726,7 +723,8 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) { spacer(); } if (service.runtime?.cachedLabel) { - const env = (service.command?.environment ?? process.env) as NodeJS.ProcessEnv; + const env = (service.command?.environment ?? + process.env) as NodeJS.ProcessEnv; const label = resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE); defaultRuntime.error( errorText( @@ -782,7 +780,8 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) { ); } if (process.platform === "linux") { - const env = (service.command?.environment ?? process.env) as NodeJS.ProcessEnv; + const env = (service.command?.environment ?? + process.env) as NodeJS.ProcessEnv; const unit = resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE); defaultRuntime.error( errorText( diff --git a/src/commands/onboard-non-interactive.ts b/src/commands/onboard-non-interactive.ts index da379a14d..ec99b0709 100644 --- a/src/commands/onboard-non-interactive.ts +++ b/src/commands/onboard-non-interactive.ts @@ -505,15 +505,15 @@ export async function runNonInteractiveOnboarding( runtime: daemonRuntimeRaw, nodePath, }); - const environment = buildServiceEnvironment({ - env: process.env, - port, - token: gatewayToken, - launchdLabel: - process.platform === "darwin" - ? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE) - : undefined, - }); + const environment = buildServiceEnvironment({ + env: process.env, + port, + token: gatewayToken, + launchdLabel: + process.platform === "darwin" + ? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE) + : undefined, + }); await service.install({ env: process.env, stdout: process.stdout, diff --git a/src/daemon/constants.test.ts b/src/daemon/constants.test.ts index 20476c405..5c3fd742d 100644 --- a/src/daemon/constants.test.ts +++ b/src/daemon/constants.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "vitest"; import { + formatGatewayServiceDescription, GATEWAY_LAUNCH_AGENT_LABEL, GATEWAY_SYSTEMD_SERVICE_NAME, GATEWAY_WINDOWS_TASK_NAME, - formatGatewayServiceDescription, resolveGatewayLaunchAgentLabel, resolveGatewaySystemdServiceName, resolveGatewayWindowsTaskName, @@ -149,15 +149,15 @@ describe("formatGatewayServiceDescription", () => { }); it("includes profile when set", () => { - expect( - formatGatewayServiceDescription({ profile: "work" }), - ).toBe("Clawdbot Gateway (profile: work)"); + expect(formatGatewayServiceDescription({ profile: "work" })).toBe( + "Clawdbot Gateway (profile: work)", + ); }); it("includes version when set", () => { - expect( - formatGatewayServiceDescription({ version: "2026.1.10" }), - ).toBe("Clawdbot Gateway (v2026.1.10)"); + expect(formatGatewayServiceDescription({ version: "2026.1.10" })).toBe( + "Clawdbot Gateway (v2026.1.10)", + ); }); it("includes profile and version when set", () => { diff --git a/src/daemon/inspect.ts b/src/daemon/inspect.ts index 82e759457..cbd45ecb7 100644 --- a/src/daemon/inspect.ts +++ b/src/daemon/inspect.ts @@ -103,7 +103,9 @@ function isClawdbotGatewayTaskName(name: string): boolean { const normalized = name.trim().toLowerCase(); if (!normalized) return false; const defaultName = resolveGatewayWindowsTaskName().toLowerCase(); - return normalized === defaultName || normalized.startsWith("clawdbot gateway"); + return ( + normalized === defaultName || normalized.startsWith("clawdbot gateway") + ); } function tryExtractPlistLabel(contents: string): string | null { diff --git a/src/daemon/launchd.ts b/src/daemon/launchd.ts index 11827c76e..ad57d1f4e 100644 --- a/src/daemon/launchd.ts +++ b/src/daemon/launchd.ts @@ -5,9 +5,9 @@ import { promisify } from "node:util"; import { colorize, isRich, theme } from "../terminal/theme.js"; import { + formatGatewayServiceDescription, GATEWAY_LAUNCH_AGENT_LABEL, LEGACY_GATEWAY_LAUNCH_AGENT_LABELS, - formatGatewayServiceDescription, resolveGatewayLaunchAgentLabel, } from "./constants.js"; import { parseKeyValueOutput } from "./runtime-parse.js"; @@ -190,12 +190,11 @@ export function buildLaunchAgentPlist({ WorkingDirectory ${plistEscape(workingDirectory)}` : ""; - const commentXml = - comment && comment.trim() - ? ` + const commentXml = comment?.trim() + ? ` Comment ${plistEscape(comment.trim())}` - : ""; + : ""; const envXml = renderEnvDict(environment); return ` diff --git a/src/daemon/schtasks.ts b/src/daemon/schtasks.ts index 65b2b7617..58eb2f420 100644 --- a/src/daemon/schtasks.ts +++ b/src/daemon/schtasks.ts @@ -5,8 +5,8 @@ import { promisify } from "node:util"; import { colorize, isRich, theme } from "../terminal/theme.js"; import { - LEGACY_GATEWAY_WINDOWS_TASK_NAMES, formatGatewayServiceDescription, + LEGACY_GATEWAY_WINDOWS_TASK_NAMES, resolveGatewayWindowsTaskName, } from "./constants.js"; import { parseKeyValueOutput } from "./runtime-parse.js"; diff --git a/src/daemon/service.ts b/src/daemon/service.ts index 973474c15..e1722618d 100644 --- a/src/daemon/service.ts +++ b/src/daemon/service.ts @@ -103,7 +103,10 @@ export function resolveGatewayService(): GatewayService { await uninstallSystemdService(args); }, stop: async (args) => { - await stopSystemdService({ stdout: args.stdout, profile: args.profile }); + await stopSystemdService({ + stdout: args.stdout, + profile: args.profile, + }); }, restart: async (args) => { await restartSystemdService({ diff --git a/src/daemon/systemd.ts b/src/daemon/systemd.ts index 03d5afd9c..5d0d35f87 100644 --- a/src/daemon/systemd.ts +++ b/src/daemon/systemd.ts @@ -6,8 +6,8 @@ import { promisify } from "node:util"; import { runCommandWithTimeout, runExec } from "../process/exec.js"; import { colorize, isRich, theme } from "../terminal/theme.js"; import { - LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES, formatGatewayServiceDescription, + LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES, resolveGatewaySystemdServiceName, } from "./constants.js"; import { parseKeyValueOutput } from "./runtime-parse.js";