From 17ef7b3b0e8895ae2dc37917b936bcad048dde28 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 5 Jan 2026 07:07:17 +0100 Subject: [PATCH] fix: status runtime + help --- CHANGELOG.md | 1 + src/auto-reply/command-detection.ts | 4 ++- src/auto-reply/reply.triggers.test.ts | 18 ++++++++++++ src/auto-reply/reply.ts | 1 + src/auto-reply/reply/commands.ts | 23 +++++++++++++-- src/auto-reply/status.test.ts | 2 ++ src/auto-reply/status.ts | 41 +++++++++++++++++++++++---- 7 files changed, 81 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4b407666..ac42a454a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Auth: add OpenAI Codex OAuth support and migrate legacy oauth.json into auth.json. - Docs: clarify auth storage, migration, and OpenAI Codex OAuth onboarding. - Sandbox: copy inbound media into sandbox workspaces so agent tools can read attachments. +- Status: show runtime (docker/direct) and move shortcuts to `/help`. ### Maintenance - Deps: bump pi-* stack, Slack SDK, discord-api-types, file-type, zod, and Biome. diff --git a/src/auto-reply/command-detection.ts b/src/auto-reply/command-detection.ts index 88f5643b6..1148732af 100644 --- a/src/auto-reply/command-detection.ts +++ b/src/auto-reply/command-detection.ts @@ -1,7 +1,9 @@ const CONTROL_COMMAND_RE = - /(?:^|\s)\/(?:status|thinking|think|t|verbose|v|elevated|elev|model|queue|activation|send|restart|reset|new)(?=$|\s|:)\b/i; + /(?:^|\s)\/(?:status|help|thinking|think|t|verbose|v|elevated|elev|model|queue|activation|send|restart|reset|new)(?=$|\s|:)\b/i; const CONTROL_COMMAND_EXACT = new Set([ + "help", + "/help", "status", "/status", "restart", diff --git a/src/auto-reply/reply.triggers.test.ts b/src/auto-reply/reply.triggers.test.ts index 3f68b62e4..bb1c8543a 100644 --- a/src/auto-reply/reply.triggers.test.ts +++ b/src/auto-reply/reply.triggers.test.ts @@ -126,6 +126,24 @@ describe("trigger handling", () => { }); }); + it("returns help without invoking the agent", async () => { + await withTempHome(async (home) => { + const res = await getReplyFromConfig( + { + Body: "/help", + From: "+1002", + To: "+2000", + }, + {}, + makeCfg(home), + ); + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toContain("Help"); + expect(text).toContain("Shortcuts"); + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + }); + }); + it("allows owner to set send policy", async () => { await withTempHome(async (home) => { const cfg = { diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index a1b083302..703884bfb 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -472,6 +472,7 @@ export async function getReplyFromConfig( defaultGroupActivation: () => defaultActivation, resolvedThinkLevel, resolvedVerboseLevel: resolvedVerboseLevel ?? "off", + resolvedElevatedLevel, resolveDefaultThinkingLevel: modelState.resolveDefaultThinkingLevel, provider, model, diff --git a/src/auto-reply/reply/commands.ts b/src/auto-reply/reply/commands.ts index 7c9971c46..19328b0c0 100644 --- a/src/auto-reply/reply/commands.ts +++ b/src/auto-reply/reply/commands.ts @@ -15,9 +15,9 @@ import { parseActivationCommand, } from "../group-activation.js"; import { parseSendPolicyCommand } from "../send-policy.js"; -import { buildStatusMessage } from "../status.js"; +import { buildHelpMessage, buildStatusMessage } from "../status.js"; import type { MsgContext } from "../templating.js"; -import type { ThinkLevel, VerboseLevel } from "../thinking.js"; +import type { ElevatedLevel, ThinkLevel, VerboseLevel } from "../thinking.js"; import type { ReplyPayload } from "../types.js"; import { isAbortTrigger, setAbortMemory } from "./abort.js"; import type { InlineDirectives } from "./directive-handling.js"; @@ -121,6 +121,7 @@ export async function handleCommands(params: { defaultGroupActivation: () => "always" | "mention"; resolvedThinkLevel?: ThinkLevel; resolvedVerboseLevel: VerboseLevel; + resolvedElevatedLevel?: ElevatedLevel; resolveDefaultThinkingLevel: () => Promise; provider: string; model: string; @@ -143,6 +144,7 @@ export async function handleCommands(params: { defaultGroupActivation, resolvedThinkLevel, resolvedVerboseLevel, + resolvedElevatedLevel, resolveDefaultThinkingLevel, model, contextTokens, @@ -260,6 +262,20 @@ export async function handleCommands(params: { }; } + const helpRequested = + command.commandBodyNormalized === "/help" || + command.commandBodyNormalized === "help" || + /(?:^|\s)\/help(?=$|\s|:)\b/i.test(command.commandBodyNormalized); + if (helpRequested) { + if (!command.isAuthorizedSender) { + logVerbose( + `Ignoring /help from unauthorized sender: ${command.senderE164 || ""}`, + ); + return { shouldContinue: false }; + } + return { shouldContinue: false, reply: { text: buildHelpMessage() } }; + } + const statusRequested = directives.hasStatusDirective || command.commandBodyNormalized === "/status" || @@ -281,10 +297,12 @@ export async function handleCommands(params: { : undefined; const statusText = buildStatusMessage({ agent: { + ...(cfg.agent ?? {}), model, contextTokens, thinkingDefault: cfg.agent?.thinkingDefault, verboseDefault: cfg.agent?.verboseDefault, + elevatedDefault: cfg.agent?.elevatedDefault, }, workspaceDir, sessionEntry, @@ -295,6 +313,7 @@ export async function handleCommands(params: { resolvedThink: resolvedThinkLevel ?? (await resolveDefaultThinkingLevel()), resolvedVerbose: resolvedVerboseLevel, + resolvedElevated: resolvedElevatedLevel, webLinked, webAuthAgeMs, heartbeatSeconds, diff --git a/src/auto-reply/status.test.ts b/src/auto-reply/status.test.ts index ca3c85715..c9139a3cc 100644 --- a/src/auto-reply/status.test.ts +++ b/src/auto-reply/status.test.ts @@ -36,12 +36,14 @@ describe("buildStatusMessage", () => { expect(text).toContain("⚙️ Status"); expect(text).toContain("Agent: embedded pi"); + expect(text).toContain("Runtime: direct"); expect(text).toContain("Context: 16k/32k (50%)"); expect(text).toContain("Session: main"); expect(text).toContain("Web: linked"); expect(text).toContain("heartbeat 45s"); expect(text).toContain("thinking=medium"); expect(text).toContain("verbose=off"); + expect(text).not.toContain("Shortcuts:"); }); it("handles missing agent config gracefully", () => { diff --git a/src/auto-reply/status.ts b/src/auto-reply/status.ts index 67458ce80..7d9459739 100644 --- a/src/auto-reply/status.ts +++ b/src/auto-reply/status.ts @@ -15,11 +15,12 @@ import { import type { ClawdbotConfig } from "../config/config.js"; import { resolveSessionTranscriptPath, + resolveMainSessionKey, type SessionEntry, type SessionScope, } from "../config/sessions.js"; import { shortenHomePath } from "../utils.js"; -import type { ThinkLevel, VerboseLevel } from "./thinking.js"; +import type { ElevatedLevel, ThinkLevel, VerboseLevel } from "./thinking.js"; type AgentConfig = NonNullable; @@ -33,6 +34,7 @@ type StatusArgs = { groupActivation?: "mention" | "always"; resolvedThink?: ThinkLevel; resolvedVerbose?: VerboseLevel; + resolvedElevated?: ElevatedLevel; now?: number; webLinked?: boolean; webAuthAgeMs?: number | null; @@ -164,8 +166,29 @@ export function buildStatusMessage(args: StatusArgs): string { const verboseLevel = args.resolvedVerbose ?? args.agent?.verboseDefault ?? "off"; const elevatedLevel = + args.resolvedElevated ?? args.sessionEntry?.elevatedLevel ?? args.agent?.elevatedDefault ?? "on"; + const runtime = (() => { + const sandboxMode = args.agent?.sandbox?.mode ?? "off"; + if (sandboxMode === "off") + return { line: "Runtime: direct", sandboxed: false }; + const sessionScope = args.sessionScope ?? "per-sender"; + const mainKey = resolveMainSessionKey({ + session: { scope: sessionScope }, + }); + const sessionKey = args.sessionKey?.trim(); + const sandboxed = sessionKey + ? sandboxMode === "all" || sessionKey !== mainKey.trim() + : false; + const runtime = sandboxed ? "docker" : sessionKey ? "direct" : "unknown"; + const suffix = sandboxed ? ` • elevated ${elevatedLevel}` : ""; + return { + line: `Runtime: ${runtime} (sandbox ${sandboxMode})${suffix}`, + sandboxed, + }; + })(); + const webLine = (() => { if (args.webLinked === false) { return "Web: not linked — run `clawdbot login` to scan the QR."; @@ -204,7 +227,9 @@ export function buildStatusMessage(args: StatusArgs): string { contextTokens ?? null, )}${entry?.abortedLastRun ? " • last run aborted" : ""}`; - const optionsLine = `Options: thinking=${thinkLevel} | verbose=${verboseLevel} | elevated=${elevatedLevel} (set with /think , /verbose on|off, /elevated on|off, /model )`; + const optionsLine = runtime.sandboxed + ? `Options: thinking=${thinkLevel} | verbose=${verboseLevel} | elevated=${elevatedLevel} (set with /think , /verbose on|off, /elevated on|off, /model )` + : `Options: thinking=${thinkLevel} | verbose=${verboseLevel} (set with /think , /verbose on|off, /model )`; const modelLabel = model ? `${provider}/${model}` : "unknown"; @@ -214,17 +239,21 @@ export function buildStatusMessage(args: StatusArgs): string { ? `Workspace: ${shortenHomePath(args.workspaceDir)}` : undefined; - const helpersLine = "Shortcuts: /new reset | /restart relink"; - return [ "⚙️ Status", webLine, agentLine, + runtime.line, workspaceLine, contextLine, sessionLine, groupActivationLine, optionsLine, - helpersLine, - ].join("\n"); + ] + .filter(Boolean) + .join("\n"); +} + +export function buildHelpMessage(): string { + return ["ℹ️ Help", "Shortcuts: /new reset | /restart relink"].join("\n"); }