This repository has been archived on 2026-05-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
risv3-relay/relay/__main__.py
ac 540b4f5b01 feat: relay daemon skeleton — queue, dispatch, conversation, ntfy (#1)
First-PR scope from #1. Single-process Python daemon that relays
between Claude Code instances and chat-Claude (Anthropic API).

Components:

* relay.config — .env + config.yaml loader. Auto-generates ntfy
  topic on first run and persists it back to .env.
* relay.state — atomic file I/O via tempfile + rename, advisory
  flock at state/.lock to enforce single-instance.
* relay.conversation — append-only history with summarization.
  Triggers a summarize call when total chars exceed
  HISTORY_CHAR_CAP (default 400k); replaces history with the
  summary plus the most recent 10 turns.
* relay.anthropic_client — SDK wrapper. Marks the system prompt
  cacheable (5-min ephemeral cache); concatenates text blocks;
  estimates per-call cost from the Anthropic price table with
  cache-write/read accounted for.
* relay.queue — JSON envelope intake; oldest-by-mtime;
  malformed envelopes moved to queue/.rejected/.
* relay.dispatch — one-input-at-a-time per session
  (dispatch/<session_id>/input.txt). Won't overwrite a pending
  dispatch; queues internally and waits for CC to delete.
* relay.ntfy — best-effort POST to https://ntfy.sh/<topic>;
  failures logged but never block the main loop.
* relay.daemon — main loop. Polls jc_input.txt (priority) then
  queue/. Detects [NEEDS-JC] in the first 200 chars of any
  response and pauses dispatch until JC writes jc_input.txt.
  JC override supports @session-N: prefix for direct dispatch
  without an API call.
* relay.__main__ — CLI: relay run / relay status / relay topic.

Tests: 57 unit tests pass (config, state, conversation, queue,
dispatch, anthropic_client, ntfy, full daemon loop with a fake
client). One real-API smoke test marked real_api, opt-in via
pytest -m real_api; skips cleanly on credit-balance errors.

Out of scope for this PR (deferred to follow-ups): Flask status
endpoint, multi-session config in production, exponential
backoff, systemd unit, cost-tracking aggregation.

Closes #1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:24:47 +00:00

76 lines
2.2 KiB
Python

"""CLI entry point: ``relay <command>``."""
from __future__ import annotations
import argparse
import json
import logging
import signal
import sys
from relay.config import load_settings
from relay.daemon import Daemon
from relay.logs import configure as configure_logs
from relay.ntfy import topic_url
from relay.state import read_json
def _cmd_run(args: argparse.Namespace) -> int:
settings = load_settings()
configure_logs(settings.logs_dir, level=logging.DEBUG if args.verbose else logging.INFO)
daemon = Daemon(settings)
def _stop(signum: int, _frame: object) -> None:
logging.getLogger(__name__).info("received signal %s; shutting down", signum)
daemon.stop()
signal.signal(signal.SIGINT, _stop)
signal.signal(signal.SIGTERM, _stop)
try:
daemon.run()
except RuntimeError as exc:
# Most commonly the lock file: another daemon is running.
print(f"error: {exc}", file=sys.stderr)
return 1
return 0
def _cmd_status(_args: argparse.Namespace) -> int:
settings = load_settings()
status_path = settings.state_dir / "status.json"
payload = read_json(status_path, default=None)
if payload is None:
print("no status file yet — has the daemon run?", file=sys.stderr)
return 1
print(json.dumps(payload, indent=2, sort_keys=False))
return 0
def _cmd_topic(_args: argparse.Namespace) -> int:
settings = load_settings()
print(topic_url(settings.ntfy_topic))
return 0
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="relay", description="risv3-relay daemon")
sub = parser.add_subparsers(dest="cmd", required=True)
run = sub.add_parser("run", help="Run the daemon in the foreground")
run.add_argument("-v", "--verbose", action="store_true")
run.set_defaults(func=_cmd_run)
status = sub.add_parser("status", help="Print the current daemon status")
status.set_defaults(func=_cmd_status)
topic = sub.add_parser("topic", help="Print the ntfy subscription URL")
topic.set_defaults(func=_cmd_topic)
args = parser.parse_args(argv)
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())