Skip to content

Comments

feat(signal): add Signal channel via signal-cli#630

Open
achton wants to merge 7 commits intosipeed:mainfrom
achton:feat/signal-channel
Open

feat(signal): add Signal channel via signal-cli#630
achton wants to merge 7 commits intosipeed:mainfrom
achton:feat/signal-channel

Conversation

@achton
Copy link
Contributor

@achton achton commented Feb 22, 2026

📝 Description

Adds Signal as a messaging channel, implementing the signal-cli approach proposed in #41 (Option A).

PicoClaw connects to a signal-cli daemon running in HTTP mode. Inbound messages arrive via SSE (/api/v1/events), outbound messages are sent via JSON-RPC (/api/v1/rpc). No CGO or additional compiled dependencies are needed in PicoClaw itself.

Key design points:

  • signal-cli as external dependency — mature, well-maintained, available as Docker image, runs on any platform including ARM SBCs
  • Compound senderID (+phone|DisplayName) — follows the same pattern as the Telegram channel for consistent allowlist behavior via BaseChannel.IsAllowed()
  • Markdown-to-Signal formatting — converts **bold**, *italic*, `code`, ```blocks```, ~~strike~~ to signal-cli textStyle ranges with correct UTF-16 code unit positions
  • Configurable group supportgroups_enabled / dms_enabled as optional booleans (default: enabled when omitted), rather than hardcoding group behavior
  • No license complications — signal-cli runs as a separate process, keeping PicoClaw fully MIT
  • Test coverage — 42 test cases across markdown-to-Signal conversion (including bold, italic, strikethrough, monospace, links, mixed styles), isGroupChat() heuristic, MIME type extension mapping, and signal-cli SSE event deserialization

Note: Aware of the channel system refactoring in #621 — happy to adapt to the new interfaces once that lands.

🗣️ Type of Change

  • 🐞 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 📖 Documentation update
  • ⚡ Code refactoring (no functional changes, no api changes)

🤖 AI Code Generation

  • 🤖 Fully AI-generated (100% AI, 0% Human)
  • 🛠️ Mostly AI-generated (AI draft, Human verified/modified)
  • 👨‍💻 Mostly Human-written (Human lead, AI assisted or none)

🔗 Related Issue

Closes #41

📚 Technical Context (Skip for Docs)

  • Reference URL: https://github.com/AsamK/signal-cli/wiki/HTTP-API
  • Reasoning: signal-cli provides a stable HTTP/JSON-RPC interface to Signal, avoiding the need for native libsignal compilation (Rust toolchain, CGO, platform-specific linking). This is especially important for embedded/ARM deployments where cross-compilation of native crypto libraries is impractical.

🧪 Test Environment

  • Hardware: Linux PC (x86_64), Raspberry Pi 5 8GB (ARM64)
  • OS: Debian 12 (Bookworm), Raspberry Pi OS Lite
  • Model/Provider: OpenRouter (Kimi K2.5)
  • Channels: Signal (DMs, tested with multiple users)

📸 Evidence (Optional)

Click to view startup and message handling logs
[INFO] signal: Starting Signal channel {signal_cli_url=http://127.0.0.1:8090, account=+***}
[INFO] signal: Signal channel started
[INFO] signal: SSE connected successfully
[INFO] agent: Processing message from signal:+***|User: Hello, what time is it? {chat_id=+***, sender_id=+***|User, channel=signal}
[INFO] agent: Routed message {agent_id=default, session_key=agent:default:main, matched_by=binding.peer}
[INFO] agent: LLM response without tool calls (direct answer) {agent_id=default, iteration=1, content_chars=42}
[INFO] agent: Response: It's currently **14:18 UTC** on Saturday. {iterations=1, final_length=42}

☑️ Checklist

  • My code/docs follow the style of this project.
  • I have performed a self-review of my own changes.
  • I have updated the documentation accordingly.

@nikolasdehor
Copy link

Note: #603 also proposes native Signal support. Both address the same feature request. Suggesting the maintainer coordinate or pick one to avoid duplicated effort.

achton and others added 6 commits February 24, 2026 13:07
Implements Signal messaging channel (sipeed#41) using signal-cli's HTTP API:
- SSE event streaming for receiving messages
- JSON-RPC for sending messages
- Typing indicators with continuous loop
- Message chunking for Signal's ~6000 char limit
- Image/voice/file attachment handling via signal-cli HTTP API
- Voice transcription via Groq (same pattern as Telegram/Discord)
- Group chat support with configurable group_policy
- DM policy (allowlist/open/disabled)
- Compound senderID format (+phone|displayName) for Bindings matching
- Automatic SSE reconnection on disconnect

Config: SignalConfig with enabled, account, signal_cli_url, allow_from,
dm_policy, group_policy fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signal doesn't interpret markdown natively. Convert **bold**,
*italic*, and ~~strikethrough~~ to signal-cli textStyle ranges.
Also strips heading markers and converts list markers to bullets.

Follows the same pattern as Telegram's markdownToTelegramHTML but
outputs signal-cli positional style ranges instead of HTML tags.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Table-driven tests covering bold, italic, strikethrough, unmatched
markers, heading stripping, list markers, blockquotes, Danish
characters, and mixed formatting with line-level patterns.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Pass compound senderID to HandleMessage() to fix dual allowlist
  check mismatch (same bug as Telegram sipeed#310)
- Replace dm_policy/group_policy strings with dms_enabled/groups_enabled
  pointer bools to match upstream config conventions
- Add MONOSPACE style support for inline code and code blocks
- Add markdown link conversion [text](url) → text (url)
- Add tests for extensionFromMIME and SSE event deserialization

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Signal setup guide, config reference, and channel table entry.
Follows the same collapsible <details> pattern as other channels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
markdownToSignal() only converted dash list markers (- item) to
bullets but not asterisk markers (* item). The leading * was passed
to the italic parser which couldn't find a closing match, leaving
a literal asterisk in the Signal message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@achton achton force-pushed the feat/signal-channel branch from 2a9fb90 to 76bb177 Compare February 24, 2026 20:45
Align struct field padding in SignalConfig and signalEnvelope to
satisfy golangci-lint gci formatting checks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feat: Add Signal channel integration

2 participants