From 1f19ca16658adeac09ed746b46288338c48f0668 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 9 Dec 2025 17:24:25 +0000 Subject: [PATCH] chore: drop runner shim and add committer helper --- AGENTS.md | 3 + CHANGELOG.md | 2 +- Swabble/README.md | 2 +- .../{HookRunner.swift => HookExecutor.swift} | 2 +- .../swabble/Commands/ServeCommand.swift | 4 +- .../swabble/Commands/TestHookCommand.swift | 4 +- Swabble/docs/spec.md | 2 +- .../Sources/Clawdis/GeneralSettings.swift | 2 +- .../Sources/Clawdis/RelayEnvironment.swift | 2 +- ...{ShellRunner.swift => ShellExecutor.swift} | 2 +- apps/macos/Sources/Clawdis/XPCService.swift | 2 +- docs/AGENTS.default.md | 2 +- docs/agent-send.md | 2 +- docs/architecture.md | 2 +- docs/clawdis-mac.md | 2 +- docs/grammy.md | 2 +- docs/mac/health.md | 2 +- scripts/clawlog.sh | 2 +- scripts/committer | 107 ++++++++++++++++++ src/auto-reply/command-reply.test.ts | 12 -- src/auto-reply/command-reply.ts | 7 +- src/auto-reply/reply.triggers.test.ts | 16 ++- src/auto-reply/reply.ts | 3 - src/commands/agent.ts | 2 - src/web/inbound.media.test.ts | 2 +- 25 files changed, 139 insertions(+), 51 deletions(-) rename Swabble/Sources/SwabbleCore/Hooks/{HookRunner.swift => HookExecutor.swift} (98%) rename apps/macos/Sources/Clawdis/{ShellRunner.swift => ShellExecutor.swift} (99%) create mode 100755 scripts/committer diff --git a/AGENTS.md b/AGENTS.md index 2b14f8349..d8d86a7cf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,5 @@ +READ ~/Projects/agent-scripts/AGENTS.MD BEFORE ANYTHING (skip if missing). + # Repository Guidelines ## Project Structure & Module Organization @@ -25,6 +27,7 @@ - Pure test additions/fixes generally do **not** need a changelog entry unless they alter user-facing behavior or the user asks for one. ## Commit & Pull Request Guidelines +- Create commits with `scripts/committer "" `; avoid manual `git add`/`git commit` so staging stays scoped. - Follow concise, action-oriented commit messages (e.g., `CLI: add verbose flag to send`). - Group related changes; avoid bundling unrelated refactors. - PRs should summarize scope, note testing performed, and mention any user-facing changes or new flags. diff --git a/CHANGELOG.md b/CHANGELOG.md index 6adfd9bd7..974bc0710 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,7 +45,7 @@ First Clawdis release after the Warelay rebrand. This is a semver-major because - Removed Twilio support and all related commands/options (webhook/up/provider flags/wait-poll); CLAWDIS is Baileys Web-only. ### Changes -- Default agent handling now favors Pi RPC while falling back to the plain command runner for non-Pi invocations, keeping heartbeat/session plumbing intact. +- Default agent handling now favors Pi RPC while falling back to plain command execution for non-Pi invocations, keeping heartbeat/session plumbing intact. - Documentation updated to reflect Pi-only support and to mark legacy Claude paths as historical. - Status command reports web session health + session recipients; config paths are locked to `~/.clawdis` with session metadata stored under `~/.clawdis/sessions/`. - Simplified send/agent/relay/heartbeat to web-only delivery; removed Twilio mocks/tests and dead code. diff --git a/Swabble/README.md b/Swabble/README.md index 3fe168616..4e645727a 100644 --- a/Swabble/README.md +++ b/Swabble/README.md @@ -30,7 +30,7 @@ swift run swabble transcribe /path/to/audio.m4a --format srt --output out.srt ``` ## Use as a library -Add swabble as a SwiftPM dependency and import the `Swabble` product to reuse the Speech pipeline, config loader, hook runner, and transcript store in your own app: +Add swabble as a SwiftPM dependency and import the `Swabble` product to reuse the Speech pipeline, config loader, hook executor, and transcript store in your own app: ```swift // Package.swift diff --git a/Swabble/Sources/SwabbleCore/Hooks/HookRunner.swift b/Swabble/Sources/SwabbleCore/Hooks/HookExecutor.swift similarity index 98% rename from Swabble/Sources/SwabbleCore/Hooks/HookRunner.swift rename to Swabble/Sources/SwabbleCore/Hooks/HookExecutor.swift index e50740116..dd59c43bb 100644 --- a/Swabble/Sources/SwabbleCore/Hooks/HookRunner.swift +++ b/Swabble/Sources/SwabbleCore/Hooks/HookExecutor.swift @@ -10,7 +10,7 @@ public struct HookJob: Sendable { } } -public actor HookRunner { +public actor HookExecutor { private let config: SwabbleConfig private var lastRun: Date? private let hostname: String diff --git a/Swabble/Sources/swabble/Commands/ServeCommand.swift b/Swabble/Sources/swabble/Commands/ServeCommand.swift index b9af6d650..6f5ede950 100644 --- a/Swabble/Sources/swabble/Commands/ServeCommand.swift +++ b/Swabble/Sources/swabble/Commands/ServeCommand.swift @@ -46,8 +46,8 @@ struct ServeCommand: ParsableCommand { } let stripped = Self.stripWake(text: seg.text, cfg: cfg) let job = HookJob(text: stripped, timestamp: Date()) - let runner = HookRunner(config: cfg) - try await runner.run(job: job) + let executor = HookExecutor(config: cfg) + try await executor.run(job: job) if cfg.transcripts.enabled { await TranscriptsStore.shared.append(text: stripped) } diff --git a/Swabble/Sources/swabble/Commands/TestHookCommand.swift b/Swabble/Sources/swabble/Commands/TestHookCommand.swift index 6cb4ae3c6..226776ceb 100644 --- a/Swabble/Sources/swabble/Commands/TestHookCommand.swift +++ b/Swabble/Sources/swabble/Commands/TestHookCommand.swift @@ -21,8 +21,8 @@ struct TestHookCommand: ParsableCommand { mutating func run() async throws { let cfg = try ConfigLoader.load(at: configURL) - let runner = HookRunner(config: cfg) - try await runner.run(job: HookJob(text: text, timestamp: Date())) + let executor = HookExecutor(config: cfg) + try await executor.run(job: HookJob(text: text, timestamp: Date())) print("hook invoked") } diff --git a/Swabble/docs/spec.md b/Swabble/docs/spec.md index aa97c1722..e9eda7de4 100644 --- a/Swabble/docs/spec.md +++ b/Swabble/docs/spec.md @@ -18,7 +18,7 @@ Goal: brabble-style always-on voice hook for macOS 26 using Apple Speech.framewo - **Config**: `SwabbleConfig` Codable. Fields: audio device name/index, wake (enabled/word/aliases/sensitivity placeholder), hook (command/args/prefix/cooldown/min_chars/timeout/env), logging (level, format), transcripts (enabled, max kept), speech (locale, enableEtiquetteReplacements flag). Stored JSON; default written by `setup`. - **Audio + Speech pipeline**: `SpeechPipeline` wraps `AVAudioEngine` input → `SpeechAnalyzer` with `SpeechTranscriber` module. Emits partial/final transcripts via async stream. Requests `.audioTimeRange` when transcripts enabled. Handles Speech permission and asset download prompts ahead of capture. - **Wake gate**: text-based keyword match against latest partial/final; strips wake term before hook dispatch. `--no-wake` disables. -- **Hook runner**: async `HookRunner` spawns `Process` with configured args, prefix substitution `${hostname}`. Enforces cooldown + timeout; injects env `SWABBLE_TEXT`, `SWABBLE_PREFIX` plus user env map. +- **Hook executor**: async `HookExecutor` spawns `Process` with configured args, prefix substitution `${hostname}`. Enforces cooldown + timeout; injects env `SWABBLE_TEXT`, `SWABBLE_PREFIX` plus user env map. - **Transcripts store**: in-memory ring buffer; optional persisted JSON lines under `~/Library/Application Support/swabble/transcripts.log`. - **Logging**: simple structured logger to stderr; respects log level. diff --git a/apps/macos/Sources/Clawdis/GeneralSettings.swift b/apps/macos/Sources/Clawdis/GeneralSettings.swift index e4c1e7873..f246a7614 100644 --- a/apps/macos/Sources/Clawdis/GeneralSettings.swift +++ b/apps/macos/Sources/Clawdis/GeneralSettings.swift @@ -466,7 +466,7 @@ extension GeneralSettings { } // Step 1: basic SSH reachability check - let sshResult = await ShellRunner.run( + let sshResult = await ShellExecutor.run( command: Self.sshCheckCommand(target: settings.target, identity: settings.identity), cwd: nil, env: nil, diff --git a/apps/macos/Sources/Clawdis/RelayEnvironment.swift b/apps/macos/Sources/Clawdis/RelayEnvironment.swift index 8967b89bd..a8e9f13cc 100644 --- a/apps/macos/Sources/Clawdis/RelayEnvironment.swift +++ b/apps/macos/Sources/Clawdis/RelayEnvironment.swift @@ -129,7 +129,7 @@ enum RelayEnvironment { let cmd = [pnpm, "add", "-g", "clawdis@\(target)"] statusHandler("Installing clawdis@\(target) via pnpm…") - let response = await ShellRunner.run(command: cmd, cwd: nil, env: ["PATH": preferred], timeout: 300) + let response = await ShellExecutor.run(command: cmd, cwd: nil, env: ["PATH": preferred], timeout: 300) if response.ok { statusHandler("Installed clawdis@\(target)") } else { diff --git a/apps/macos/Sources/Clawdis/ShellRunner.swift b/apps/macos/Sources/Clawdis/ShellExecutor.swift similarity index 99% rename from apps/macos/Sources/Clawdis/ShellRunner.swift rename to apps/macos/Sources/Clawdis/ShellExecutor.swift index ff16b2321..580b098fb 100644 --- a/apps/macos/Sources/Clawdis/ShellRunner.swift +++ b/apps/macos/Sources/Clawdis/ShellExecutor.swift @@ -1,7 +1,7 @@ import ClawdisIPC import Foundation -enum ShellRunner { +enum ShellExecutor { static func run(command: [String], cwd: String?, env: [String: String]?, timeout: Double?) async -> Response { guard !command.isEmpty else { return Response(ok: false, message: "empty command") } diff --git a/apps/macos/Sources/Clawdis/XPCService.swift b/apps/macos/Sources/Clawdis/XPCService.swift index f8f63bce7..46a30931a 100644 --- a/apps/macos/Sources/Clawdis/XPCService.swift +++ b/apps/macos/Sources/Clawdis/XPCService.swift @@ -70,7 +70,7 @@ final class ClawdisXPCService: NSObject, ClawdisXPCProtocol { .ensure([.screenRecording], interactive: false)[.screenRecording] ?? false guard authorized else { return Response(ok: false, message: "screen recording permission missing") } } - return await ShellRunner.run(command: command, cwd: cwd, env: env, timeout: timeoutSec) + return await ShellExecutor.run(command: command, cwd: cwd, env: env, timeout: timeoutSec) case let .agent(message, thinking, session, deliver, to): let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/docs/AGENTS.default.md b/docs/AGENTS.default.md index 886cd20a4..a3e11db60 100644 --- a/docs/AGENTS.default.md +++ b/docs/AGENTS.default.md @@ -9,7 +9,7 @@ - **mcporter** — MCP runtime/CLI to list, call, and sync Model Context Protocol servers. - **Peekaboo** — Fast macOS screenshots with optional AI vision analysis. - **camsnap** — Capture frames, clips, or motion alerts from RTSP/ONVIF security cams. -- **oracle** — OpenAI-ready agent runner with session replay and browser control. +- **oracle** — OpenAI-ready agent CLI with session replay and browser control. - **eightctl** — Control Eight Sleep Pod temperature, alarms, schedules, and metrics. - **imsg** — macOS Messages CLI to read/tail chats and send iMessage/SMS. - **spotify-player** — Terminal Spotify client to search/queue/control playback. diff --git a/docs/agent-send.md b/docs/agent-send.md index 1b07cde4b..27b9c84ff 100644 --- a/docs/agent-send.md +++ b/docs/agent-send.md @@ -1,6 +1,6 @@ # Plan: `clawdis agent` (direct-to-agent invocation) -Goal: Add a CLI subcommand that talks directly to the configured agent/command runner (no WhatsApp send), while reusing the same session handling and config clawdis already uses for auto-replies. +Goal: Add a CLI subcommand that talks directly to the configured agent command (no WhatsApp send), while reusing the same session handling and config clawdis already uses for auto-replies. ## Why - Sometimes we want to poke the agent directly (same prompt templates/sessions) without sending a WhatsApp message. diff --git a/docs/architecture.md b/docs/architecture.md index 899cb4eaf..c6aaedf54 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -15,7 +15,7 @@ Last updated: 2025-12-09 - **Clients (mac app / CLI / web admin)** - One WS connection per client. - Send requests (`health`, `status`, `send`, `agent`, `system-presence`, toggles) and subscribe to events (`tick`, `agent`, `presence`, `shutdown`). -- **Agent runner (Tau/Pi process)** +- **Agent process (Tau/Pi)** - Spawned by the Gateway on demand for `agent` calls; streams events back over the same WS connection. - **WebChat** - Serves static assets locally. diff --git a/docs/clawdis-mac.md b/docs/clawdis-mac.md index cc3ab84a8..9d15fa953 100644 --- a/docs/clawdis-mac.md +++ b/docs/clawdis-mac.md @@ -53,7 +53,7 @@ struct Response { ok: Bool; message?: String; payload?: Data } - NotificationManager: UNUserNotificationCenter primary; AppleScript `display notification` fallback; respects the `--sound` value on each request. - PermissionManager: checks/requests Notifications, Accessibility (AX), Screen Recording (capture probe); publishes changes for UI. - ScreenCaptureManager: window/display PNG capture; gated on permission. -- ShellRunner: executes `Process` with timeout; rejects when `needsScreenRecording` and permission missing; returns stdout/stderr in payload. +- ShellExecutor: executes `Process` with timeout; rejects when `needsScreenRecording` and permission missing; returns stdout/stderr in payload. - XPCListener actor: routes Request → managers; logs via OSLog. ## CLI (`clawdis-mac`) diff --git a/docs/grammy.md b/docs/grammy.md index c8e1cb768..80926b67b 100644 --- a/docs/grammy.md +++ b/docs/grammy.md @@ -3,7 +3,7 @@ Updated: 2025-12-07 # Why grammY -- TS-first Bot API client with built-in long-poll + webhook runners, middleware, error handling, rate limiter. +- TS-first Bot API client with built-in long-poll + webhook helpers, middleware, error handling, rate limiter. - Cleaner media helpers than hand-rolling fetch + FormData; supports all Bot API methods. - Extensible: proxy support via custom fetch, session middleware (optional), type-safe context. diff --git a/docs/mac/health.md b/docs/mac/health.md index faff5e2de..5f0b5a20f 100644 --- a/docs/mac/health.md +++ b/docs/mac/health.md @@ -15,7 +15,7 @@ How to see whether the WhatsApp Web/Baileys bridge is healthy from the menu bar - Uses a cached snapshot so the UI loads instantly and falls back gracefully when offline. ## How the probe works -- App runs `clawdis health --json` via `ShellRunner` every ~60s and on demand. The probe loads creds, attempts a short Baileys connect, and reports status without sending messages. +- App runs `clawdis health --json` via `ShellExecutor` every ~60s and on demand. The probe loads creds, attempts a short Baileys connect, and reports status without sending messages. - Cache the last good snapshot and the last error separately to avoid flicker; show the timestamp of each. ## When in doubt diff --git a/scripts/clawlog.sh b/scripts/clawlog.sh index 2259a2271..b0f8e97f5 100755 --- a/scripts/clawlog.sh +++ b/scripts/clawlog.sh @@ -66,7 +66,7 @@ LOG CATEGORIES (examples): • xpc - XPC service calls • notifications - Notification helper • screenshot - Screenshotter - • shell - ShellRunner + • shell - ShellExecutor QUICK START: vtlog -n 100 Show last 100 lines from all components diff --git a/scripts/committer b/scripts/committer new file mode 100755 index 000000000..230b23fb0 --- /dev/null +++ b/scripts/committer @@ -0,0 +1,107 @@ +#!/usr/bin/env bash + +set -euo pipefail +# Disable glob expansion to handle brackets in file paths +set -f +usage() { + printf 'Usage: %s [--force] "commit message" "file" ["file" ...]\n' "$(basename "$0")" >&2 + exit 2 +} + +if [ "$#" -lt 2 ]; then + usage +fi + +force_delete_lock=false +if [ "${1:-}" = "--force" ]; then + force_delete_lock=true + shift +fi + +if [ "$#" -lt 2 ]; then + usage +fi + +commit_message=$1 +shift + +if [[ "$commit_message" != *[![:space:]]* ]]; then + printf 'Error: commit message must not be empty\n' >&2 + exit 1 +fi + +if [ -e "$commit_message" ]; then + printf 'Error: first argument looks like a file path ("%s"); provide the commit message first\n' "$commit_message" >&2 + exit 1 +fi + +if [ "$#" -eq 0 ]; then + usage +fi + +files=("$@") + +# Disallow "." because it stages the entire repository and defeats the helper's safety guardrails. +for file in "${files[@]}"; do + if [ "$file" = "." ]; then + printf 'Error: "." is not allowed; list specific paths instead\n' >&2 + exit 1 + fi +done + +last_commit_error='' + +run_git_commit() { + local stderr_log + stderr_log=$(mktemp) + if git commit -m "$commit_message" -- "${files[@]}" 2> >(tee "$stderr_log" >&2); then + rm -f "$stderr_log" + last_commit_error='' + return 0 + fi + + last_commit_error=$(cat "$stderr_log") + rm -f "$stderr_log" + return 1 +} + +for file in "${files[@]}"; do + if [ ! -e "$file" ]; then + if ! git ls-files --error-unmatch -- "$file" >/dev/null 2>&1; then + printf 'Error: file not found: %s\n' "$file" >&2 + exit 1 + fi + fi +done + +git restore --staged :/ +git add --force -- "${files[@]}" + +if git diff --staged --quiet; then + printf 'Warning: no staged changes detected for: %s\n' "${files[*]}" >&2 + exit 1 +fi + +committed=false +if run_git_commit; then + committed=true +elif [ "$force_delete_lock" = true ]; then + lock_path=$( + printf '%s\n' "$last_commit_error" | + awk -F"'" '/Unable to create .*\.git\/index\.lock/ { print $2; exit }' + ) + + if [ -n "$lock_path" ] && [ -e "$lock_path" ]; then + rm -f "$lock_path" + printf 'Removed stale git lock: %s\n' "$lock_path" >&2 + if run_git_commit; then + committed=true + fi + fi +fi + +if [ "$committed" = false ]; then + exit 1 +fi + +printf 'Committed "%s" with %d files\n' "$commit_message" "${#files[@]}" diff --git a/src/auto-reply/command-reply.test.ts b/src/auto-reply/command-reply.test.ts index a7811ed30..c7310a000 100644 --- a/src/auto-reply/command-reply.test.ts +++ b/src/auto-reply/command-reply.test.ts @@ -62,7 +62,6 @@ describe("runCommandReply (pi)", () => { systemSent: false, timeoutMs: 1000, timeoutSeconds: 1, - commandRunner: vi.fn(), enqueue: enqueueImmediate, thinkLevel: "medium", }); @@ -100,7 +99,6 @@ describe("runCommandReply (pi)", () => { systemSent: false, timeoutMs: 1000, timeoutSeconds: 1, - commandRunner: vi.fn(), enqueue: enqueueImmediate, }); @@ -142,7 +140,6 @@ describe("runCommandReply (pi)", () => { systemSent: false, timeoutMs: 1000, timeoutSeconds: 1, - commandRunner: vi.fn(), enqueue: enqueueImmediate, }); @@ -183,7 +180,6 @@ describe("runCommandReply (pi)", () => { systemSent: false, timeoutMs: 1000, timeoutSeconds: 1, - commandRunner: vi.fn(), enqueue: enqueueImmediate, }); @@ -240,7 +236,6 @@ describe("runCommandReply (pi)", () => { systemSent: false, timeoutMs: 1000, timeoutSeconds: 1, - commandRunner: vi.fn(), enqueue: enqueueImmediate, onAgentEvent: (evt) => events.push(evt), }); @@ -281,7 +276,6 @@ describe("runCommandReply (pi)", () => { systemSent: true, timeoutMs: 1000, timeoutSeconds: 1, - commandRunner: vi.fn(), enqueue: enqueueImmediate, }); @@ -311,7 +305,6 @@ describe("runCommandReply (pi)", () => { systemSent: false, timeoutMs: 10, timeoutSeconds: 1, - commandRunner: vi.fn(), enqueue: enqueueImmediate, }); @@ -344,7 +337,6 @@ describe("runCommandReply (pi)", () => { systemSent: false, timeoutMs: 1000, timeoutSeconds: 1, - commandRunner: vi.fn(), enqueue: enqueueImmediate, }); @@ -379,7 +371,6 @@ describe("runCommandReply (pi)", () => { systemSent: false, timeoutMs: 1000, timeoutSeconds: 1, - commandRunner: vi.fn(), enqueue: enqueueImmediate, }); @@ -411,7 +402,6 @@ describe("runCommandReply (pi)", () => { systemSent: false, timeoutMs: 1000, timeoutSeconds: 1, - commandRunner: vi.fn(), enqueue: enqueueImmediate, onPartialReply: onPartial, verboseLevel: "off", @@ -445,7 +435,6 @@ describe("runCommandReply (pi)", () => { systemSent: false, timeoutMs: 1000, timeoutSeconds: 1, - commandRunner: vi.fn(), enqueue: enqueueImmediate, }); @@ -475,7 +464,6 @@ describe("runCommandReply (pi)", () => { systemSent: false, timeoutMs: 100, timeoutSeconds: 1, - commandRunner: vi.fn(), enqueue: enqueueImmediate, }); diff --git a/src/auto-reply/command-reply.ts b/src/auto-reply/command-reply.ts index 1bc06f35e..cd5d5b924 100644 --- a/src/auto-reply/command-reply.ts +++ b/src/auto-reply/command-reply.ts @@ -15,7 +15,6 @@ import { logError } from "../logger.js"; import { getChildLogger } from "../logging.js"; import { splitMediaFromOutput } from "../media/parse.js"; import { enqueueCommand } from "../process/command-queue.js"; -import type { runCommandWithTimeout } from "../process/exec.js"; import { runPiRpc } from "../process/tau-rpc.js"; import { applyTemplate, type TemplateContext } from "./templating.js"; import { @@ -146,7 +145,7 @@ type CommandReplyConfig = NonNullable["reply"] & { mode: "command"; }; -type EnqueueRunner = typeof enqueueCommand; +type EnqueueCommandFn = typeof enqueueCommand; type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high"; @@ -159,8 +158,7 @@ type CommandReplyParams = { systemSent: boolean; timeoutMs: number; timeoutSeconds: number; - commandRunner: typeof runCommandWithTimeout; - enqueue?: EnqueueRunner; + enqueue?: EnqueueCommandFn; thinkLevel?: ThinkLevel; verboseLevel?: "off" | "on"; onPartialReply?: (payload: ReplyPayload) => Promise | void; @@ -347,7 +345,6 @@ export async function runCommandReply( systemSent, timeoutMs, timeoutSeconds, - commandRunner: _commandRunner, enqueue = enqueueCommand, thinkLevel, verboseLevel, diff --git a/src/auto-reply/reply.triggers.test.ts b/src/auto-reply/reply.triggers.test.ts index 85b474fdc..7c5c05ea2 100644 --- a/src/auto-reply/reply.triggers.test.ts +++ b/src/auto-reply/reply.triggers.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import * as tauRpc from "../process/tau-rpc.js"; +import * as commandReply from "./command-reply.js"; import { getReplyFromConfig } from "./reply.js"; const webMocks = vi.hoisted(() => ({ @@ -27,7 +28,7 @@ afterEach(() => { describe("trigger handling", () => { it("aborts even with timestamp prefix", async () => { - const runner = vi.fn(); + const commandSpy = vi.spyOn(commandReply, "runCommandReply"); const res = await getReplyFromConfig( { Body: "[Dec 5 10:00] stop", @@ -36,15 +37,14 @@ describe("trigger handling", () => { }, {}, baseCfg, - runner, ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("⚙️ Agent was aborted."); - expect(runner).not.toHaveBeenCalled(); + expect(commandSpy).not.toHaveBeenCalled(); }); it("restarts even with prefix/whitespace", async () => { - const runner = vi.fn(); + const commandSpy = vi.spyOn(commandReply, "runCommandReply"); const res = await getReplyFromConfig( { Body: " [Dec 5] /restart", @@ -53,15 +53,14 @@ describe("trigger handling", () => { }, {}, baseCfg, - runner, ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text?.startsWith("⚙️ Restarting" ?? "")).toBe(true); - expect(runner).not.toHaveBeenCalled(); + expect(commandSpy).not.toHaveBeenCalled(); }); it("reports status without invoking the agent", async () => { - const runner = vi.fn(); + const commandSpy = vi.spyOn(commandReply, "runCommandReply"); const res = await getReplyFromConfig( { Body: "/status", @@ -70,11 +69,10 @@ describe("trigger handling", () => { }, {}, baseCfg, - runner, ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toContain("Status"); - expect(runner).not.toHaveBeenCalled(); + expect(commandSpy).not.toHaveBeenCalled(); }); it("ignores think directives that only appear in the context wrapper", async () => { diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index c30d285a5..77a51bce2 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -17,7 +17,6 @@ import { isVerbose, logVerbose } from "../globals.js"; import { buildProviderSummary } from "../infra/provider-summary.js"; import { triggerWarelayRestart } from "../infra/restart.js"; import { drainSystemEvents } from "../infra/system-events.js"; -import { runCommandWithTimeout } from "../process/exec.js"; import { defaultRuntime } from "../runtime.js"; import { resolveHeartbeatSeconds } from "../web/reconnect.js"; import { getWebAuthAgeMs, webAuthExists } from "../web/session.js"; @@ -163,7 +162,6 @@ export async function getReplyFromConfig( ctx: MsgContext, opts?: GetReplyOptions, configOverride?: WarelayConfig, - commandRunner: typeof runCommandWithTimeout = runCommandWithTimeout, ): Promise { // Choose reply from config: static text or external command stdout. const cfg = configOverride ?? loadConfig(); @@ -737,7 +735,6 @@ export async function getReplyFromConfig( systemSent, timeoutMs, timeoutSeconds, - commandRunner, thinkLevel: resolvedThinkLevel, verboseLevel: resolvedVerboseLevel, onPartialReply: opts?.onPartialReply, diff --git a/src/commands/agent.ts b/src/commands/agent.ts index d1e6b8a23..c4d89da56 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -22,7 +22,6 @@ import { saveSessionStore, } from "../config/sessions.js"; import { emitAgentEvent } from "../infra/agent-events.js"; -import { runCommandWithTimeout } from "../process/exec.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { normalizeE164 } from "../utils.js"; @@ -319,7 +318,6 @@ export async function agentCommand( systemSent, timeoutMs, timeoutSeconds, - commandRunner: runCommandWithTimeout, thinkLevel: resolvedThinkLevel, verboseLevel: resolvedVerboseLevel, runId: sessionId, diff --git a/src/web/inbound.media.test.ts b/src/web/inbound.media.test.ts index b9d0e9875..79a6fd83d 100644 --- a/src/web/inbound.media.test.ts +++ b/src/web/inbound.media.test.ts @@ -101,7 +101,7 @@ describe("web inbound media saves with extension", () => { realSock.ev.emit("messages.upsert", upsert); - // Allow a brief window for the async handler to fire on slower runners. + // Allow a brief window for the async handler to fire on slower hosts. for (let i = 0; i < 10; i++) { if (onMessage.mock.calls.length > 0) break; await new Promise((resolve) => setTimeout(resolve, 5));