Files
Keysat 8ad1cd8465 Add headless "ask" mode: ?-prefixed message runs claude -p, answer posted back
A message starting with `?` in a mapped room runs `claude -p` one-shot in that
repo on the Mac and posts the full answer back into the room — Matrix as a
request/response interface, not just a trigger. Non-`?` messages keep launching
interactive sessions as before.

New scripts/ask-claude.sh is a login-shell wrapper (so ~/.zprofile puts claude on
PATH) that exports CLAUDE_CODE_OAUTH_TOKEN from the Mac's .env and runs
`claude -p "$prompt" < /dev/null`, printing the answer to stdout. The bot adds a
`?`-dispatch with run_ask/ask: SSH stdout captured, 300s timeout, fail-loud, output
chunked under Matrix's event cap (no truncation).

Headless claude -p needs the long-lived token because a non-GUI SSH session can't
reach the login Keychain (reports "Not logged in") — the deliberate Approach A that
the interactive GUI-Terminal path (D11) avoided. Token is kept Mac-side only; the
Spark never runs claude. Sovereignty unchanged: claude -p uses the subscription, no
frontier API touches message payloads.

Proven live on the Spark; fresh-eyes reviewed before commit.
2026-06-15 19:50:36 -05:00

46 lines
1.9 KiB
Bash
Executable File

#!/bin/zsh -l
# ask-claude.sh — matrix-bridge headless "ask" wrapper.
#
# Invoked over SSH by the bot: ask-claude.sh <repo_dir> <prompt...>
# Runs `claude -p` one-shot in the repo and prints the answer to STDOUT, which the bot
# captures over the SSH pipe and posts back into the Matrix room. Unlike launch-claude.sh /
# gui-launch.sh (interactive, surfaced to the phone), this NEVER opens a GUI Terminal.
#
# Two seams it owns, both proven the hard way in Phase 0:
# - LOGIN shell (-l): a non-login SSH shell loads neither ~/.zprofile nor ~/.zshrc, so
# ~/.local/bin isn't on PATH and `claude` isn't found. Same reason as launch-claude.sh.
# - Headless auth via CLAUDE_CODE_OAUTH_TOKEN (from `claude setup-token`, stored in ../.env):
# a non-GUI SSH session can't reach the login Keychain, so plain `claude -p` reports
# "Not logged in" (D11 / Approach A). We export the token to bypass the Keychain.
set -e
script_dir="${0:A:h}"
# Pull just the token out of ../.env (don't `source` the whole file — other values, e.g. a
# password, may not be shell-safe). Absent token => claude reports "Not logged in", reported
# back to the room by the bot.
env_file="$script_dir/../.env"
if [[ -f "$env_file" ]]; then
token_line="$(grep -E '^CLAUDE_CODE_OAUTH_TOKEN=' "$env_file" | head -1)"
token="${token_line#*=}"
token="${token#\"}" # strip one surrounding quote pair if present (KEY="value")
token="${token%\"}"
export CLAUDE_CODE_OAUTH_TOKEN="$token"
fi
repo_dir="$1"
shift
prompt="$*"
if [[ -z "$repo_dir" || -z "$prompt" ]]; then
print -u2 "usage: ask-claude.sh <repo_dir> <prompt>"
exit 2
fi
# Fail loud on a bad directory — never run Claude in the wrong place.
cd "$repo_dir" || { print -u2 "ask-claude: no such repo dir: $repo_dir"; exit 1; }
# < /dev/null: print mode reads stdin by default and otherwise stalls ~3s waiting for it.
exec claude -p "$prompt" < /dev/null