From 664bf01ceaccf730cb636463cc8587cd1d966192 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Fri, 29 May 2026 14:51:53 -0500 Subject: feat(signal): page-signal CLI wrapper + workflows + cross-project broadcast helper Three coupled additions ship together. claude-templates/bin/page-signal is a bash wrapper around signal-cli send. It defaults to --note-to-self for safety. The wrapper supports --file for attachments, --to <+number> for outbound (explicit per call, no defaults, no batch), --quiet, and --json. Exit codes: 0 sent, 1 signal-cli failure, 2 usage error, 3 signal-cli not installed. claude-templates/.ai/workflows/page-signal.org carries the discrimination rules and safety rails. When desktop notify covers it, don't reach for Signal. Long-running task completion is the canonical case. Outbound to other contacts requires explicit Craig instruction per send. A known-limitation note covers the current notification gap. signal-cli registered on Craig's primary number means messages don't fire notifications until the pending Google Voice registration lands. claude-templates/.ai/workflows/cross-project-broadcast.org and its helper cross-project-broadcast.py fan out a single message file to every AI project's inbox in one operation. Discovery is fingerprint-based: any directory under ~/code, ~/projects, ~/.emacs.d with both .ai/protocols.org and a top-level inbox/ is broadcastable. Senders are auto-excluded. Verified discovery against 23 broadcastable targets. Makefile's install target gains a general bin/ loop. The previous version hardcoded bin/ai. The new version iterates over every executable under claude-templates/bin/ and symlinks each into ~/.local/bin/. install-hooks (existing Claude hook installer) is unchanged. install-githooks (sync-check pre-commit hook setup, added earlier today) is unchanged. The bin/ loop now picks up bin/page-signal automatically. INDEX entries for both new workflows landed under Tools and meta. No bats tests on the new scripts. page-signal was smoke-tested with a live send. The send succeeded. The notification gap is covered by the workflow's known-limitation note. cross-project-broadcast.py was smoke-tested via --list against the live project set. Tests can be added when the broadcast pattern proves out across multiple use cases. --- claude-templates/bin/page-signal | 126 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100755 claude-templates/bin/page-signal (limited to 'claude-templates/bin') diff --git a/claude-templates/bin/page-signal b/claude-templates/bin/page-signal new file mode 100755 index 0000000..5a87c67 --- /dev/null +++ b/claude-templates/bin/page-signal @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# +# page-signal — wrap signal-cli send for paging Craig via Signal. +# +# Defaults to --note-to-self for safety. Outbound (to another contact) +# requires an explicit --to <+number> on every call. No defaults, no batch. +# +# Usage: +# page-signal "message" # note-to-self +# page-signal --file path "message" # with attachment +# page-signal --to +15551234567 "msg" # outbound (explicit recipient) +# page-signal --quiet "message" # suppress success output +# page-signal --json "message" # structured output (timestamp + status) +# echo "msg" | page-signal # message from stdin (no positional) +# +# Requires signal-cli installed and registered with an account. +# +# Use this when desktop notifications won't reach the user — page-signal +# fires cross-device. For routine completions and periodic status pings, +# stick with `notify`; Signal is for things that need attention while away +# from the desk. + +set -euo pipefail + +quiet=0 +json=0 +file= +recipient_args=(--note-to-self) +message= + +usage() { + sed -n '3,21p' "$0" | sed 's/^# \{0,1\}//' +} + +while [ "$#" -gt 0 ]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + --quiet) + quiet=1 + shift + ;; + --json) + json=1 + shift + ;; + --file) + [ "$#" -ge 2 ] || { echo "page-signal: --file needs a path" >&2; exit 2; } + file="$2" + [ -f "$file" ] || { echo "page-signal: file not found: $file" >&2; exit 2; } + shift 2 + ;; + --to) + [ "$#" -ge 2 ] || { echo "page-signal: --to needs a +E.164 number" >&2; exit 2; } + case "$2" in + +[0-9]*) ;; + *) echo "page-signal: --to expects +E.164 (e.g. +15551234567)" >&2; exit 2 ;; + esac + recipient_args=("$2") + shift 2 + ;; + --) + shift + break + ;; + -*) + echo "page-signal: unknown flag: $1 (use --help)" >&2 + exit 2 + ;; + *) + message="$1" + shift + ;; + esac +done + +if ! command -v signal-cli >/dev/null 2>&1; then + echo "page-signal: signal-cli not found in PATH" >&2 + exit 3 +fi + +# Message: from positional arg, or from stdin if none given. +if [ -z "$message" ]; then + if [ -t 0 ]; then + echo "page-signal: no message (pass as arg or pipe via stdin)" >&2 + exit 2 + fi + message="$(cat)" +fi + +# Build the send command. +cmd=(signal-cli send -m "$message") +if [ -n "$file" ]; then + cmd+=(-a "$file") +fi +cmd+=("${recipient_args[@]}") + +# Send. Capture stderr so we can surface clean failure messages. +timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" +if err="$("${cmd[@]}" 2>&1 >/dev/null)"; then + status=ok +else + status=fail +fi + +if [ "$json" -eq 1 ]; then + printf '{"status":"%s","timestamp":"%s","recipient":"%s"' \ + "$status" "$timestamp" "${recipient_args[0]}" + if [ -n "$file" ]; then + printf ',"attachment":"%s"' "$file" + fi + if [ "$status" = "fail" ]; then + printf ',"error":"%s"' "$(echo "$err" | head -1 | sed 's/"/\\"/g')" + fi + printf '}\n' +elif [ "$quiet" -eq 0 ]; then + if [ "$status" = "ok" ]; then + echo "page-signal: sent to ${recipient_args[*]} at $timestamp" + else + echo "page-signal: FAIL — $err" >&2 + fi +fi + +[ "$status" = "ok" ] -- cgit v1.2.3