chore: drop runner shim and add committer helper
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
READ ~/Projects/agent-scripts/AGENTS.MD BEFORE ANYTHING (skip if missing).
|
||||||
|
|
||||||
# Repository Guidelines
|
# Repository Guidelines
|
||||||
|
|
||||||
## Project Structure & Module Organization
|
## 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.
|
- 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
|
## Commit & Pull Request Guidelines
|
||||||
|
- Create commits with `scripts/committer "<msg>" <file...>`; avoid manual `git add`/`git commit` so staging stays scoped.
|
||||||
- Follow concise, action-oriented commit messages (e.g., `CLI: add verbose flag to send`).
|
- Follow concise, action-oriented commit messages (e.g., `CLI: add verbose flag to send`).
|
||||||
- Group related changes; avoid bundling unrelated refactors.
|
- Group related changes; avoid bundling unrelated refactors.
|
||||||
- PRs should summarize scope, note testing performed, and mention any user-facing changes or new flags.
|
- PRs should summarize scope, note testing performed, and mention any user-facing changes or new flags.
|
||||||
|
|||||||
@@ -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.
|
- Removed Twilio support and all related commands/options (webhook/up/provider flags/wait-poll); CLAWDIS is Baileys Web-only.
|
||||||
|
|
||||||
### Changes
|
### 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.
|
- 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/`.
|
- 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.
|
- Simplified send/agent/relay/heartbeat to web-only delivery; removed Twilio mocks/tests and dead code.
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ swift run swabble transcribe /path/to/audio.m4a --format srt --output out.srt
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Use as a library
|
## 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
|
```swift
|
||||||
// Package.swift
|
// Package.swift
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ public struct HookJob: Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public actor HookRunner {
|
public actor HookExecutor {
|
||||||
private let config: SwabbleConfig
|
private let config: SwabbleConfig
|
||||||
private var lastRun: Date?
|
private var lastRun: Date?
|
||||||
private let hostname: String
|
private let hostname: String
|
||||||
@@ -46,8 +46,8 @@ struct ServeCommand: ParsableCommand {
|
|||||||
}
|
}
|
||||||
let stripped = Self.stripWake(text: seg.text, cfg: cfg)
|
let stripped = Self.stripWake(text: seg.text, cfg: cfg)
|
||||||
let job = HookJob(text: stripped, timestamp: Date())
|
let job = HookJob(text: stripped, timestamp: Date())
|
||||||
let runner = HookRunner(config: cfg)
|
let executor = HookExecutor(config: cfg)
|
||||||
try await runner.run(job: job)
|
try await executor.run(job: job)
|
||||||
if cfg.transcripts.enabled {
|
if cfg.transcripts.enabled {
|
||||||
await TranscriptsStore.shared.append(text: stripped)
|
await TranscriptsStore.shared.append(text: stripped)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ struct TestHookCommand: ParsableCommand {
|
|||||||
|
|
||||||
mutating func run() async throws {
|
mutating func run() async throws {
|
||||||
let cfg = try ConfigLoader.load(at: configURL)
|
let cfg = try ConfigLoader.load(at: configURL)
|
||||||
let runner = HookRunner(config: cfg)
|
let executor = HookExecutor(config: cfg)
|
||||||
try await runner.run(job: HookJob(text: text, timestamp: Date()))
|
try await executor.run(job: HookJob(text: text, timestamp: Date()))
|
||||||
print("hook invoked")
|
print("hook invoked")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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`.
|
- **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.
|
- **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.
|
- **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`.
|
- **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.
|
- **Logging**: simple structured logger to stderr; respects log level.
|
||||||
|
|
||||||
|
|||||||
@@ -466,7 +466,7 @@ extension GeneralSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: basic SSH reachability check
|
// 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),
|
command: Self.sshCheckCommand(target: settings.target, identity: settings.identity),
|
||||||
cwd: nil,
|
cwd: nil,
|
||||||
env: nil,
|
env: nil,
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ enum RelayEnvironment {
|
|||||||
let cmd = [pnpm, "add", "-g", "clawdis@\(target)"]
|
let cmd = [pnpm, "add", "-g", "clawdis@\(target)"]
|
||||||
|
|
||||||
statusHandler("Installing clawdis@\(target) via pnpm…")
|
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 {
|
if response.ok {
|
||||||
statusHandler("Installed clawdis@\(target)")
|
statusHandler("Installed clawdis@\(target)")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import ClawdisIPC
|
import ClawdisIPC
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum ShellRunner {
|
enum ShellExecutor {
|
||||||
static func run(command: [String], cwd: String?, env: [String: String]?, timeout: Double?) async -> Response {
|
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") }
|
guard !command.isEmpty else { return Response(ok: false, message: "empty command") }
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ final class ClawdisXPCService: NSObject, ClawdisXPCProtocol {
|
|||||||
.ensure([.screenRecording], interactive: false)[.screenRecording] ?? false
|
.ensure([.screenRecording], interactive: false)[.screenRecording] ?? false
|
||||||
guard authorized else { return Response(ok: false, message: "screen recording permission missing") }
|
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):
|
case let .agent(message, thinking, session, deliver, to):
|
||||||
let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
- **mcporter** — MCP runtime/CLI to list, call, and sync Model Context Protocol servers.
|
- **mcporter** — MCP runtime/CLI to list, call, and sync Model Context Protocol servers.
|
||||||
- **Peekaboo** — Fast macOS screenshots with optional AI vision analysis.
|
- **Peekaboo** — Fast macOS screenshots with optional AI vision analysis.
|
||||||
- **camsnap** — Capture frames, clips, or motion alerts from RTSP/ONVIF security cams.
|
- **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.
|
- **eightctl** — Control Eight Sleep Pod temperature, alarms, schedules, and metrics.
|
||||||
- **imsg** — macOS Messages CLI to read/tail chats and send iMessage/SMS.
|
- **imsg** — macOS Messages CLI to read/tail chats and send iMessage/SMS.
|
||||||
- **spotify-player** — Terminal Spotify client to search/queue/control playback.
|
- **spotify-player** — Terminal Spotify client to search/queue/control playback.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Plan: `clawdis agent` (direct-to-agent invocation)
|
# 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
|
## Why
|
||||||
- Sometimes we want to poke the agent directly (same prompt templates/sessions) without sending a WhatsApp message.
|
- Sometimes we want to poke the agent directly (same prompt templates/sessions) without sending a WhatsApp message.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Last updated: 2025-12-09
|
|||||||
- **Clients (mac app / CLI / web admin)**
|
- **Clients (mac app / CLI / web admin)**
|
||||||
- One WS connection per client.
|
- One WS connection per client.
|
||||||
- Send requests (`health`, `status`, `send`, `agent`, `system-presence`, toggles) and subscribe to events (`tick`, `agent`, `presence`, `shutdown`).
|
- 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.
|
- Spawned by the Gateway on demand for `agent` calls; streams events back over the same WS connection.
|
||||||
- **WebChat**
|
- **WebChat**
|
||||||
- Serves static assets locally.
|
- Serves static assets locally.
|
||||||
|
|||||||
@@ -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.
|
- 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.
|
- PermissionManager: checks/requests Notifications, Accessibility (AX), Screen Recording (capture probe); publishes changes for UI.
|
||||||
- ScreenCaptureManager: window/display PNG capture; gated on permission.
|
- 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.
|
- XPCListener actor: routes Request → managers; logs via OSLog.
|
||||||
|
|
||||||
## CLI (`clawdis-mac`)
|
## CLI (`clawdis-mac`)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Updated: 2025-12-07
|
Updated: 2025-12-07
|
||||||
|
|
||||||
# Why grammY
|
# 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.
|
- 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.
|
- Extensible: proxy support via custom fetch, session middleware (optional), type-safe context.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
- Uses a cached snapshot so the UI loads instantly and falls back gracefully when offline.
|
||||||
|
|
||||||
## How the probe works
|
## 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.
|
- Cache the last good snapshot and the last error separately to avoid flicker; show the timestamp of each.
|
||||||
|
|
||||||
## When in doubt
|
## When in doubt
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ LOG CATEGORIES (examples):
|
|||||||
• xpc - XPC service calls
|
• xpc - XPC service calls
|
||||||
• notifications - Notification helper
|
• notifications - Notification helper
|
||||||
• screenshot - Screenshotter
|
• screenshot - Screenshotter
|
||||||
• shell - ShellRunner
|
• shell - ShellExecutor
|
||||||
|
|
||||||
QUICK START:
|
QUICK START:
|
||||||
vtlog -n 100 Show last 100 lines from all components
|
vtlog -n 100 Show last 100 lines from all components
|
||||||
|
|||||||
107
scripts/committer
Executable file
107
scripts/committer
Executable file
@@ -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[@]}"
|
||||||
@@ -62,7 +62,6 @@ describe("runCommandReply (pi)", () => {
|
|||||||
systemSent: false,
|
systemSent: false,
|
||||||
timeoutMs: 1000,
|
timeoutMs: 1000,
|
||||||
timeoutSeconds: 1,
|
timeoutSeconds: 1,
|
||||||
commandRunner: vi.fn(),
|
|
||||||
enqueue: enqueueImmediate,
|
enqueue: enqueueImmediate,
|
||||||
thinkLevel: "medium",
|
thinkLevel: "medium",
|
||||||
});
|
});
|
||||||
@@ -100,7 +99,6 @@ describe("runCommandReply (pi)", () => {
|
|||||||
systemSent: false,
|
systemSent: false,
|
||||||
timeoutMs: 1000,
|
timeoutMs: 1000,
|
||||||
timeoutSeconds: 1,
|
timeoutSeconds: 1,
|
||||||
commandRunner: vi.fn(),
|
|
||||||
enqueue: enqueueImmediate,
|
enqueue: enqueueImmediate,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -142,7 +140,6 @@ describe("runCommandReply (pi)", () => {
|
|||||||
systemSent: false,
|
systemSent: false,
|
||||||
timeoutMs: 1000,
|
timeoutMs: 1000,
|
||||||
timeoutSeconds: 1,
|
timeoutSeconds: 1,
|
||||||
commandRunner: vi.fn(),
|
|
||||||
enqueue: enqueueImmediate,
|
enqueue: enqueueImmediate,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -183,7 +180,6 @@ describe("runCommandReply (pi)", () => {
|
|||||||
systemSent: false,
|
systemSent: false,
|
||||||
timeoutMs: 1000,
|
timeoutMs: 1000,
|
||||||
timeoutSeconds: 1,
|
timeoutSeconds: 1,
|
||||||
commandRunner: vi.fn(),
|
|
||||||
enqueue: enqueueImmediate,
|
enqueue: enqueueImmediate,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -240,7 +236,6 @@ describe("runCommandReply (pi)", () => {
|
|||||||
systemSent: false,
|
systemSent: false,
|
||||||
timeoutMs: 1000,
|
timeoutMs: 1000,
|
||||||
timeoutSeconds: 1,
|
timeoutSeconds: 1,
|
||||||
commandRunner: vi.fn(),
|
|
||||||
enqueue: enqueueImmediate,
|
enqueue: enqueueImmediate,
|
||||||
onAgentEvent: (evt) => events.push(evt),
|
onAgentEvent: (evt) => events.push(evt),
|
||||||
});
|
});
|
||||||
@@ -281,7 +276,6 @@ describe("runCommandReply (pi)", () => {
|
|||||||
systemSent: true,
|
systemSent: true,
|
||||||
timeoutMs: 1000,
|
timeoutMs: 1000,
|
||||||
timeoutSeconds: 1,
|
timeoutSeconds: 1,
|
||||||
commandRunner: vi.fn(),
|
|
||||||
enqueue: enqueueImmediate,
|
enqueue: enqueueImmediate,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -311,7 +305,6 @@ describe("runCommandReply (pi)", () => {
|
|||||||
systemSent: false,
|
systemSent: false,
|
||||||
timeoutMs: 10,
|
timeoutMs: 10,
|
||||||
timeoutSeconds: 1,
|
timeoutSeconds: 1,
|
||||||
commandRunner: vi.fn(),
|
|
||||||
enqueue: enqueueImmediate,
|
enqueue: enqueueImmediate,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -344,7 +337,6 @@ describe("runCommandReply (pi)", () => {
|
|||||||
systemSent: false,
|
systemSent: false,
|
||||||
timeoutMs: 1000,
|
timeoutMs: 1000,
|
||||||
timeoutSeconds: 1,
|
timeoutSeconds: 1,
|
||||||
commandRunner: vi.fn(),
|
|
||||||
enqueue: enqueueImmediate,
|
enqueue: enqueueImmediate,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -379,7 +371,6 @@ describe("runCommandReply (pi)", () => {
|
|||||||
systemSent: false,
|
systemSent: false,
|
||||||
timeoutMs: 1000,
|
timeoutMs: 1000,
|
||||||
timeoutSeconds: 1,
|
timeoutSeconds: 1,
|
||||||
commandRunner: vi.fn(),
|
|
||||||
enqueue: enqueueImmediate,
|
enqueue: enqueueImmediate,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -411,7 +402,6 @@ describe("runCommandReply (pi)", () => {
|
|||||||
systemSent: false,
|
systemSent: false,
|
||||||
timeoutMs: 1000,
|
timeoutMs: 1000,
|
||||||
timeoutSeconds: 1,
|
timeoutSeconds: 1,
|
||||||
commandRunner: vi.fn(),
|
|
||||||
enqueue: enqueueImmediate,
|
enqueue: enqueueImmediate,
|
||||||
onPartialReply: onPartial,
|
onPartialReply: onPartial,
|
||||||
verboseLevel: "off",
|
verboseLevel: "off",
|
||||||
@@ -445,7 +435,6 @@ describe("runCommandReply (pi)", () => {
|
|||||||
systemSent: false,
|
systemSent: false,
|
||||||
timeoutMs: 1000,
|
timeoutMs: 1000,
|
||||||
timeoutSeconds: 1,
|
timeoutSeconds: 1,
|
||||||
commandRunner: vi.fn(),
|
|
||||||
enqueue: enqueueImmediate,
|
enqueue: enqueueImmediate,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -475,7 +464,6 @@ describe("runCommandReply (pi)", () => {
|
|||||||
systemSent: false,
|
systemSent: false,
|
||||||
timeoutMs: 100,
|
timeoutMs: 100,
|
||||||
timeoutSeconds: 1,
|
timeoutSeconds: 1,
|
||||||
commandRunner: vi.fn(),
|
|
||||||
enqueue: enqueueImmediate,
|
enqueue: enqueueImmediate,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { logError } from "../logger.js";
|
|||||||
import { getChildLogger } from "../logging.js";
|
import { getChildLogger } from "../logging.js";
|
||||||
import { splitMediaFromOutput } from "../media/parse.js";
|
import { splitMediaFromOutput } from "../media/parse.js";
|
||||||
import { enqueueCommand } from "../process/command-queue.js";
|
import { enqueueCommand } from "../process/command-queue.js";
|
||||||
import type { runCommandWithTimeout } from "../process/exec.js";
|
|
||||||
import { runPiRpc } from "../process/tau-rpc.js";
|
import { runPiRpc } from "../process/tau-rpc.js";
|
||||||
import { applyTemplate, type TemplateContext } from "./templating.js";
|
import { applyTemplate, type TemplateContext } from "./templating.js";
|
||||||
import {
|
import {
|
||||||
@@ -146,7 +145,7 @@ type CommandReplyConfig = NonNullable<WarelayConfig["inbound"]>["reply"] & {
|
|||||||
mode: "command";
|
mode: "command";
|
||||||
};
|
};
|
||||||
|
|
||||||
type EnqueueRunner = typeof enqueueCommand;
|
type EnqueueCommandFn = typeof enqueueCommand;
|
||||||
|
|
||||||
type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high";
|
type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high";
|
||||||
|
|
||||||
@@ -159,8 +158,7 @@ type CommandReplyParams = {
|
|||||||
systemSent: boolean;
|
systemSent: boolean;
|
||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
timeoutSeconds: number;
|
timeoutSeconds: number;
|
||||||
commandRunner: typeof runCommandWithTimeout;
|
enqueue?: EnqueueCommandFn;
|
||||||
enqueue?: EnqueueRunner;
|
|
||||||
thinkLevel?: ThinkLevel;
|
thinkLevel?: ThinkLevel;
|
||||||
verboseLevel?: "off" | "on";
|
verboseLevel?: "off" | "on";
|
||||||
onPartialReply?: (payload: ReplyPayload) => Promise<void> | void;
|
onPartialReply?: (payload: ReplyPayload) => Promise<void> | void;
|
||||||
@@ -347,7 +345,6 @@ export async function runCommandReply(
|
|||||||
systemSent,
|
systemSent,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
timeoutSeconds,
|
timeoutSeconds,
|
||||||
commandRunner: _commandRunner,
|
|
||||||
enqueue = enqueueCommand,
|
enqueue = enqueueCommand,
|
||||||
thinkLevel,
|
thinkLevel,
|
||||||
verboseLevel,
|
verboseLevel,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import * as tauRpc from "../process/tau-rpc.js";
|
import * as tauRpc from "../process/tau-rpc.js";
|
||||||
|
import * as commandReply from "./command-reply.js";
|
||||||
import { getReplyFromConfig } from "./reply.js";
|
import { getReplyFromConfig } from "./reply.js";
|
||||||
|
|
||||||
const webMocks = vi.hoisted(() => ({
|
const webMocks = vi.hoisted(() => ({
|
||||||
@@ -27,7 +28,7 @@ afterEach(() => {
|
|||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
it("aborts even with timestamp prefix", async () => {
|
it("aborts even with timestamp prefix", async () => {
|
||||||
const runner = vi.fn();
|
const commandSpy = vi.spyOn(commandReply, "runCommandReply");
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
Body: "[Dec 5 10:00] stop",
|
Body: "[Dec 5 10:00] stop",
|
||||||
@@ -36,15 +37,14 @@ describe("trigger handling", () => {
|
|||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
baseCfg,
|
baseCfg,
|
||||||
runner,
|
|
||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toBe("⚙️ Agent was aborted.");
|
expect(text).toBe("⚙️ Agent was aborted.");
|
||||||
expect(runner).not.toHaveBeenCalled();
|
expect(commandSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("restarts even with prefix/whitespace", async () => {
|
it("restarts even with prefix/whitespace", async () => {
|
||||||
const runner = vi.fn();
|
const commandSpy = vi.spyOn(commandReply, "runCommandReply");
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
Body: " [Dec 5] /restart",
|
Body: " [Dec 5] /restart",
|
||||||
@@ -53,15 +53,14 @@ describe("trigger handling", () => {
|
|||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
baseCfg,
|
baseCfg,
|
||||||
runner,
|
|
||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text?.startsWith("⚙️ Restarting" ?? "")).toBe(true);
|
expect(text?.startsWith("⚙️ Restarting" ?? "")).toBe(true);
|
||||||
expect(runner).not.toHaveBeenCalled();
|
expect(commandSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reports status without invoking the agent", async () => {
|
it("reports status without invoking the agent", async () => {
|
||||||
const runner = vi.fn();
|
const commandSpy = vi.spyOn(commandReply, "runCommandReply");
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
Body: "/status",
|
Body: "/status",
|
||||||
@@ -70,11 +69,10 @@ describe("trigger handling", () => {
|
|||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
baseCfg,
|
baseCfg,
|
||||||
runner,
|
|
||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toContain("Status");
|
expect(text).toContain("Status");
|
||||||
expect(runner).not.toHaveBeenCalled();
|
expect(commandSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores think directives that only appear in the context wrapper", async () => {
|
it("ignores think directives that only appear in the context wrapper", async () => {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { isVerbose, logVerbose } from "../globals.js";
|
|||||||
import { buildProviderSummary } from "../infra/provider-summary.js";
|
import { buildProviderSummary } from "../infra/provider-summary.js";
|
||||||
import { triggerWarelayRestart } from "../infra/restart.js";
|
import { triggerWarelayRestart } from "../infra/restart.js";
|
||||||
import { drainSystemEvents } from "../infra/system-events.js";
|
import { drainSystemEvents } from "../infra/system-events.js";
|
||||||
import { runCommandWithTimeout } from "../process/exec.js";
|
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
import { resolveHeartbeatSeconds } from "../web/reconnect.js";
|
import { resolveHeartbeatSeconds } from "../web/reconnect.js";
|
||||||
import { getWebAuthAgeMs, webAuthExists } from "../web/session.js";
|
import { getWebAuthAgeMs, webAuthExists } from "../web/session.js";
|
||||||
@@ -163,7 +162,6 @@ export async function getReplyFromConfig(
|
|||||||
ctx: MsgContext,
|
ctx: MsgContext,
|
||||||
opts?: GetReplyOptions,
|
opts?: GetReplyOptions,
|
||||||
configOverride?: WarelayConfig,
|
configOverride?: WarelayConfig,
|
||||||
commandRunner: typeof runCommandWithTimeout = runCommandWithTimeout,
|
|
||||||
): Promise<ReplyPayload | ReplyPayload[] | undefined> {
|
): Promise<ReplyPayload | ReplyPayload[] | undefined> {
|
||||||
// Choose reply from config: static text or external command stdout.
|
// Choose reply from config: static text or external command stdout.
|
||||||
const cfg = configOverride ?? loadConfig();
|
const cfg = configOverride ?? loadConfig();
|
||||||
@@ -737,7 +735,6 @@ export async function getReplyFromConfig(
|
|||||||
systemSent,
|
systemSent,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
timeoutSeconds,
|
timeoutSeconds,
|
||||||
commandRunner,
|
|
||||||
thinkLevel: resolvedThinkLevel,
|
thinkLevel: resolvedThinkLevel,
|
||||||
verboseLevel: resolvedVerboseLevel,
|
verboseLevel: resolvedVerboseLevel,
|
||||||
onPartialReply: opts?.onPartialReply,
|
onPartialReply: opts?.onPartialReply,
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
saveSessionStore,
|
saveSessionStore,
|
||||||
} from "../config/sessions.js";
|
} from "../config/sessions.js";
|
||||||
import { emitAgentEvent } from "../infra/agent-events.js";
|
import { emitAgentEvent } from "../infra/agent-events.js";
|
||||||
import { runCommandWithTimeout } from "../process/exec.js";
|
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||||
import { normalizeE164 } from "../utils.js";
|
import { normalizeE164 } from "../utils.js";
|
||||||
|
|
||||||
@@ -319,7 +318,6 @@ export async function agentCommand(
|
|||||||
systemSent,
|
systemSent,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
timeoutSeconds,
|
timeoutSeconds,
|
||||||
commandRunner: runCommandWithTimeout,
|
|
||||||
thinkLevel: resolvedThinkLevel,
|
thinkLevel: resolvedThinkLevel,
|
||||||
verboseLevel: resolvedVerboseLevel,
|
verboseLevel: resolvedVerboseLevel,
|
||||||
runId: sessionId,
|
runId: sessionId,
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ describe("web inbound media saves with extension", () => {
|
|||||||
|
|
||||||
realSock.ev.emit("messages.upsert", upsert);
|
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++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
if (onMessage.mock.calls.length > 0) break;
|
if (onMessage.mock.calls.length > 0) break;
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||||
|
|||||||
Reference in New Issue
Block a user