From 7e5b3958ccbb4719c5f2db81fbbb259b7bf9e196 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 26 Nov 2025 18:00:23 +0100 Subject: [PATCH] CLI: rename heartbeat tmux helper and log file path --- CHANGELOG.md | 3 ++- README.md | 4 ++-- docs/heartbeat.md | 3 ++- docs/tmux.md | 17 +++++++++++++++++ src/cli/program.test.ts | 10 ++++++++++ src/cli/program.ts | 7 ++++++- src/logging.ts | 5 +++++ 7 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 docs/tmux.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 28f027ddd..9e25d1f56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ ### Changes - Web relay now supports configurable command heartbeats (`inbound.reply.heartbeatMinutes`, default 30m) that ping Claude with a `HEARTBEAT_OK` sentinel; outbound messages are skipped when the token is returned, and normal/verbose logs record each heartbeat tick. - New `warelay heartbeat` CLI triggers a one-off heartbeat (web provider, auto-detects logged-in session; optional `--to` override). Relay gains `--heartbeat-now` to fire an immediate heartbeat on startup. -- Added `warelay relay:heartbeat` (no tmux) and `warelay relay:tmux:heartbeat` helpers to start relay with an immediate startup heartbeat. +- Added `warelay relay:heartbeat` (no tmux) and `warelay relay:heartbeat:tmux` helpers to start relay with an immediate startup heartbeat. +- Relay now prints the active file log path and level on startup so you can tail logs without attaching. - Heartbeat session handling now supports `inbound.reply.session.heartbeatIdleMinutes` and does not refresh `updatedAt` on skipped heartbeats, so sessions still expire on idle. ## 1.1.0 — 2025-11-26 diff --git a/README.md b/README.md index e78852c75..5ec25819f 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Install from npm (global): `npm install -g warelay` (Node 22+). Then choose **on | `warelay status` | Show recent sent/received messages | `--limit ` `--lookback ` `--json` `--verbose` | | `warelay heartbeat` | Trigger one heartbeat poll (web) | `--provider ` `--to ` `--verbose` | | `warelay relay:heartbeat` | Run relay with an immediate heartbeat (no tmux) | `--provider ` `--verbose` | -| `warelay relay:tmux:heartbeat` | Start relay in tmux and fire a heartbeat on start (web) | _no flags_ | +| `warelay relay:heartbeat:tmux` | Start relay in tmux and fire a heartbeat on start (web) | _no flags_ | | `warelay webhook` | Run inbound webhook (`ingress=tailscale` updates Twilio; `none` is local-only) | `--ingress tailscale\|none` `--port ` `--path ` `--reply ` `--verbose` `--yes` `--dry-run` | | `warelay login` | Link personal WhatsApp Web via QR | `--verbose` | @@ -125,7 +125,7 @@ Best practice: use a dedicated WhatsApp account (separate SIM/eSIM or business a - When `heartbeatMinutes` is set (default 30 for `mode: "command"`), the relay periodically runs your command/Claude session with a heartbeat prompt. - If Claude replies exactly `HEARTBEAT_OK`, the message is suppressed; otherwise the reply (or media) is forwarded. Suppressions are still logged so you know the heartbeat ran. - Override session freshness for heartbeats with `session.heartbeatIdleMinutes` (defaults to `session.idleMinutes`). Heartbeat skips do **not** bump `updatedAt`, so sessions still expire normally. -- Trigger one manually with `warelay heartbeat` (web provider only, `--verbose` prints session info). Use `warelay relay:heartbeat` for a full relay run with an immediate heartbeat, or `--heartbeat-now` on `relay`/`relay:tmux:heartbeat`. +- Trigger one manually with `warelay heartbeat` (web provider only, `--verbose` prints session info). Use `warelay relay:heartbeat` for a full relay run with an immediate heartbeat, or `--heartbeat-now` on `relay`/`relay:heartbeat:tmux`. ### Logging (optional) - File logs are written to `/tmp/warelay/warelay.log` by default. Levels: `silent | fatal | error | warn | info | debug | trace` (CLI `--verbose` forces `debug`). Web-provider inbound/outbound entries include message bodies and auto-reply text for easier auditing. diff --git a/docs/heartbeat.md b/docs/heartbeat.md index 0ba67715e..87fb71905 100644 --- a/docs/heartbeat.md +++ b/docs/heartbeat.md @@ -38,4 +38,5 @@ Goal: add a simple heartbeat poll for command-based auto-replies (Claude-driven) - Expose CLI triggers: - `warelay heartbeat` (web provider, defaults to first `allowFrom`; optional `--to` override) - `warelay relay:heartbeat` to run the relay loop with an immediate heartbeat (no tmux) - - Relay supports `--heartbeat-now` to fire once at startup (including `relay:tmux:heartbeat`). + - `warelay relay:heartbeat:tmux` to run the same in tmux (detached, attachable) + - Relay supports `--heartbeat-now` to fire once at startup (including the tmux helper). diff --git a/docs/tmux.md b/docs/tmux.md new file mode 100644 index 000000000..b2f0185d9 --- /dev/null +++ b/docs/tmux.md @@ -0,0 +1,17 @@ +# tmux helpers (relay backgrounding) + +## Why we ship tmux helpers +- Run the relay detached so your shell can close, while keeping an interactive pane you can reattach to. +- Provide a consistent start/attach workflow without adding a daemon mode or external process manager. +- Keep the relay code itself tmux-agnostic; tmux is only a launcher concern. + +## Commands +- `warelay relay:tmux` — restarts the `warelay-relay` session running `pnpm warelay relay --verbose`, then attaches (skips attach when stdout isn’t a TTY). +- `warelay relay:tmux:attach` — attach to the existing session without restarting it. +- `warelay relay:heartbeat:tmux` — same as `relay:tmux` but adds `--heartbeat-now` so Claude is pinged immediately on startup. + +All helpers use the fixed session name `warelay-relay`. + +## Logs +- The relay always writes to the configured file logger (defaults to `/tmp/warelay/warelay.log`); on start it prints the active log path and level. +- tmux is just for interactive viewing; you can also tail the log file or use another supervisor if you prefer. diff --git a/src/cli/program.test.ts b/src/cli/program.test.ts index beda2661d..c0a883e4f 100644 --- a/src/cli/program.test.ts +++ b/src/cli/program.test.ts @@ -116,4 +116,14 @@ describe("cli program", () => { expect(runtime.exit).not.toHaveBeenCalled(); runtime.exit = originalExit; }); + + it("runs relay heartbeat tmux helper", async () => { + const program = buildProgram(); + await program.parseAsync(["relay:heartbeat:tmux"], { from: "user" }); + const shouldAttach = Boolean(process.stdout.isTTY); + expect(spawnRelayTmux).toHaveBeenCalledWith( + "pnpm warelay relay --verbose --heartbeat-now", + shouldAttach, + ); + }); }); diff --git a/src/cli/program.ts b/src/cli/program.ts index bc614f633..acbd601f9 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -6,6 +6,7 @@ import { webhookCommand } from "../commands/webhook.js"; import { loadConfig } from "../config/config.js"; import { ensureTwilioEnv } from "../env.js"; import { danger, info, setVerbose, setYes } from "../globals.js"; +import { getResolvedLoggerSettings } from "../logging.js"; import { loginWeb, logoutWeb, @@ -273,6 +274,8 @@ Examples: ) .action(async (opts) => { setVerbose(Boolean(opts.verbose)); + const { file: logFile, level: logLevel } = getResolvedLoggerSettings(); + defaultRuntime.log(info(`logs: ${logFile} (level ${logLevel})`)); const providerPref = String(opts.provider ?? "auto"); if (!["auto", "web", "twilio"].includes(providerPref)) { defaultRuntime.error("--provider must be auto, web, or twilio"); @@ -406,6 +409,8 @@ Examples: .option("--verbose", "Verbose logging", false) .action(async (opts) => { setVerbose(Boolean(opts.verbose)); + const { file: logFile, level: logLevel } = getResolvedLoggerSettings(); + defaultRuntime.log(info(`logs: ${logFile} (level ${logLevel})`)); const providerPref = String(opts.provider ?? "auto"); if (!["auto", "web"].includes(providerPref)) { defaultRuntime.error("--provider must be auto or web"); @@ -584,7 +589,7 @@ Examples: }); program - .command("relay:tmux:heartbeat") + .command("relay:heartbeat:tmux") .description( "Run relay --verbose with an immediate heartbeat inside tmux (session warelay-relay), then attach", ) diff --git a/src/logging.ts b/src/logging.ts index 858a185a3..3e0a6d21b 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -28,6 +28,7 @@ type ResolvedSettings = { level: LevelWithSilent; file: string; }; +export type LoggerResolvedSettings = ResolvedSettings; let cachedLogger: Logger | null = null; let cachedSettings: ResolvedSettings | null = null; @@ -87,6 +88,10 @@ export function getChildLogger( return getLogger().child(bindings ?? {}, opts); } +export function getResolvedLoggerSettings(): LoggerResolvedSettings { + return resolveSettings(); +} + // Test helpers export function setLoggerOverride(settings: LoggerSettings | null) { overrideSettings = settings;