docs(skills): merge add-signal-v2 lessons into add-signal, drop v2

Absorbs battle-tested knowledge from the v2 skill into the upstream
add-signal: registration paths (new number + linked device), CAPTCHA
flow, VoIP SMS-first timing, Java prereq, config-lock warning, wiring
SQL for groups, not_member silent-drop fix, GroupV2 groupId extraction
note, and UUID-based platform ID format.

Corrects a factual error in the upstream: DM platform IDs are
signal:{UUID} (ACI), not phone numbers.

Removes the now-redundant add-signal-v2 skill.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ira Abramov
2026-04-25 16:52:20 +03:00
parent 7e99d0eaf7
commit b6be3b9bf4
2 changed files with 184 additions and 325 deletions

View File

@@ -1,311 +0,0 @@
# Add Signal Channel (v2)
Adds Signal messaging support to NanoClaw v2 using `signal-sdk` (a TypeScript
wrapper around `signal-cli`). Unlike Telegram/Discord, Signal has no bot API —
NanoClaw registers as a full Signal account on a dedicated phone number.
**Two registration paths:**
- **New number (recommended):** Register a dedicated SIM or VoIP number as a
standalone Signal account. NanoClaw owns the number entirely.
- **Linked device:** Join an existing Signal account as a secondary device via
QR code. Simpler, but NanoClaw shares your personal number.
Both paths are documented below. The new-number path is battle-tested.
---
## Pre-flight
Check if `src/channels/signal.ts` exists and the import is uncommented in
`src/channels/index.ts`. If both are in place, skip to Registration.
## Install
### 1. Check Java
Java 17+ is required. Check:
```bash
java -version
```
If missing:
- **RHEL/CentOS/Fedora:** `sudo dnf install -y java-17-openjdk`
- **Debian/Ubuntu:** `sudo apt-get install -y default-jre`
- **macOS:** `brew install --cask temurin@17`
Java 1725 all work. Java 25 (RHEL9 default) is confirmed working.
### 2. Install signal-cli
The `signal-sdk` npm package bundles signal-cli, but the bundled version is
often outdated. Install the latest standalone binary:
```bash
SIGNAL_CLI_VERSION=$(curl -fsSL https://api.github.com/repos/AsamK/signal-cli/releases/latest | python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'][1:])")
curl -fsSL "https://github.com/AsamK/signal-cli/releases/download/v${SIGNAL_CLI_VERSION}/signal-cli-${SIGNAL_CLI_VERSION}-Linux-native.tar.gz" \
| tar -xz -C ~/.local
ln -sf ~/.local/signal-cli ~/.local/bin/signal-cli
signal-cli --version
```
> **Note:** The Linux native tarball extracts a single binary directly to
> `~/.local/signal-cli` (not into a subdirectory). The symlink above handles this.
### 3. Install signal-sdk
```bash
npm install signal-sdk
```
### 4. Enable the adapter
Uncomment the Signal import in `src/channels/index.ts`:
```typescript
import './signal.js';
```
### 5. Build
Always build with Node 22 (nvm):
```bash
source ~/.nvm/nvm.sh && nvm use 22 && npm run build
```
---
## Path A: Register a new Signal number
Use this if you have a dedicated SIM or VoIP number for NanoClaw.
> **VoIP numbers:** Signal requires SMS verification before voice. Some VoIP
> providers are blocked even for voice calls. If registration fails with an auth
> error, try a different provider or a physical SIM.
### Step 1: Request SMS verification
Signal requires a CAPTCHA on first registration:
1. Open `https://signalcaptchas.org/registration/generate.html` in a browser
2. Solve the captcha
3. Right-click the **"Open Signal"** button → **Copy Link**
4. The link starts with `signalcaptcha://...`
```bash
SIGNAL_CLI_CONFIG_PATH=/path/to/nanoclaw/data/signal \
signal-cli -u +YOURNUMBER register \
--captcha "PASTE_CAPTCHA_TOKEN_HERE"
```
The captcha token is everything after `signalcaptcha://` in the copied link.
### Step 2: Voice call fallback (VoIP numbers without SMS)
If your number cannot receive SMS, wait ~60 seconds after the SMS request then
request a voice call:
```bash
SIGNAL_CLI_CONFIG_PATH=/path/to/nanoclaw/data/signal \
signal-cli -u +YOURNUMBER register --voice \
--captcha "SAME_CAPTCHA_TOKEN"
```
Signal will call your number and read a 6-digit code.
> The captcha token from Step 1 is reusable for the voice retry — no need to
> solve a new one.
### Step 3: Verify
```bash
SIGNAL_CLI_CONFIG_PATH=/path/to/nanoclaw/data/signal \
signal-cli -u +YOURNUMBER verify CODE
```
No output = success.
### Step 4: Set profile name (optional)
> ⚠ signal-sdk holds an exclusive lock on `data/signal/` while nanoclaw is
> running. Stop the service before running signal-cli commands, then restart.
```bash
systemctl --user stop nanoclaw
SIGNAL_CLI_CONFIG_PATH=/path/to/nanoclaw/data/signal \
signal-cli -u +YOURNUMBER updateProfile --name "YourBotName"
systemctl --user start nanoclaw
```
To set an avatar too:
```bash
signal-cli -u +YOURNUMBER updateProfile --name "YourBotName" --avatar /path/to/avatar.jpg
```
---
## Path B: Link as secondary device
Use this to join an existing Signal account as a secondary device.
```bash
mkdir -p data/signal
export SIGNAL_CLI_CONFIG_PATH=$(pwd)/data/signal
npx signal-sdk link -n "NanoClaw" -a +YOURNUMBER
```
This prints a QR code. On your phone: **Settings → Linked Devices → Link New Device**.
Scan the code within ~30 seconds.
---
## Configure environment
Add to `.env`:
```bash
SIGNAL_PHONE_NUMBER=+YOURNUMBER
```
---
## Wire to an agent
### DMs
After the service starts, **send any message** to the Signal number from your
personal Signal app. The router auto-creates a `messaging_groups` row. Then:
```bash
sqlite3 data/v2.db \
"SELECT id, platform_id FROM messaging_groups WHERE channel_type='signal' ORDER BY created_at DESC LIMIT 5"
```
Pass the `id` and `platform_id` to `/init-first-agent` or wire manually.
**Important:** DM `platform_id` is UUID-based, not phone-based:
- DM: `signal:3de71d7f-ffa3-437e-b4db-097534bccd46` (UUID of sender)
- Group: `signal:UwaIz6bc09Olg1pBr/XeBuQf6z3fyCZoNj/Y3Tpe3hI=` (base64 group ID)
### Groups
Add the Signal number to a group from your phone. Send any message — the router
auto-creates the group's `messaging_groups` row. Wire it:
```bash
sqlite3 data/v2.db \
"SELECT id, platform_id FROM messaging_groups WHERE channel_type='signal' ORDER BY created_at DESC LIMIT 5"
```
Then insert the wiring (replace IDs):
```bash
NOW=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
sqlite3 data/v2.db "
INSERT OR IGNORE INTO messaging_group_agents
(id, messaging_group_id, agent_group_id, session_mode, priority, created_at)
VALUES
('mga-'||hex(randomblob(8)), 'mg-GROUPID', 'ag-AGENTID', 'isolated', 0, '$NOW');
"
```
Use `session_mode='isolated'` for groups so each group has its own session.
### Grant user access
Users who message via Signal need to be granted membership. Without this,
messages are silently dropped with `not_member`. After first contact:
```bash
NOW=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
sqlite3 data/v2.db "
INSERT OR REPLACE INTO user_roles (user_id, role, agent_group_id, granted_by, granted_at)
VALUES ('signal:UUID', 'owner', NULL, 'system', '$NOW');
INSERT OR IGNORE INTO agent_group_members (user_id, agent_group_id, added_by, added_at)
VALUES ('signal:UUID', 'ag-AGENTID', 'system', '$NOW');
"
```
Find the UUID from `messaging_groups.platform_id` or the `users` table.
---
## Voice Transcription (optional)
Inbound voice messages are automatically transcribed using a local whisper
binary. The feature degrades gracefully — if whisper or ffmpeg is missing,
voice messages are still delivered as attachments with no transcript.
See `/add-voice-transcription-free-whisper` for full setup instructions
(ffmpeg, whisper backends, model download, environment variables, troubleshooting).
---
## Channel Info
- **type**: `signal`
- **terminology**: "Chats" (1:1) and "groups" (multi-member). No threads.
- **supports-threads**: no
- **inbound**: text, reactions (forwarded to agent with emoji + targetTimestamp), images, files, voice (transcribed via whisper-cli if installed)
- **outbound**: text, reactions (sendReaction), file attachments
- **platform-id-format**:
- DM: `signal:{UUID}` — sender's Signal UUID, **not** their phone number
- Group: `signal:{base64GroupId}`
- **how-to-find-id**: Send a message to the bot, then query `messaging_groups`
as shown above.
- **config-lock**: signal-sdk holds an exclusive lock on `data/signal/` while
nanoclaw is running. Stop the service before running any `signal-cli` commands.
- **attachment storage**: signal-sdk launches signal-cli **without** a `--config`
flag, so signal-cli stores attachments at the XDG default
(`~/.local/share/signal-cli/attachments/`), not under `data/signal/`. The
adapter checks both locations. Verify with:
`ps aux | grep signal-cli` — if there is no `-c` argument, XDG default is in use.
---
## Troubleshooting
**`Config file is in use by another instance`** — nanoclaw is running and
signal-sdk has the lock. Stop the service, run the command, restart.
**Messages dropped with `not_member`** — the Signal user hasn't been granted
membership. See "Grant user access" above. This affects every new Signal user,
including the owner's Signal identity (which is separate from their Telegram
identity even if it's the same person).
**Captcha required** — Signal requires captcha for new registrations. Go to
`https://signalcaptchas.org/registration/generate.html`, solve it, right-click
"Open Signal", copy the link, extract the token after `signalcaptcha://`.
**`Invalid verification method: Before requesting voice verification…`** —
You must request SMS first, wait ~60 seconds, then request voice. Both can use
the same captcha token.
**`The provided model identifier is invalid` (Bedrock)** — unrelated to Signal;
this is a LiteLLM model ID issue.
**Group replies going to DM instead of group** — modern Signal groups use
GroupV2. The adapter must extract the group ID from
`envelope?.dataMessage?.groupV2?.id` (not just `groupInfo?.groupId`, which is
GroupV1/legacy). Check `src/channels/signal.ts` and confirm the groupId
extraction falls through to `groupV2.id`.
**Voice messages / attachments silently skipped, no transcript** — signal-sdk
launches signal-cli without a `--config` flag, so attachments land at the XDG
default (`~/.local/share/signal-cli/attachments/`) rather than under
`data/signal/`. Confirm with `ps aux | grep signal-cli` — if there is no `-c`
argument in the process line, the XDG default is in use. The adapter falls back
to that location automatically. If you still see no "Signal attachment saved"
log lines, add a debug log around the `if (!storedPath) continue` guard in
`src/channels/signal.ts` to inspect `att.storedFilename` and `att.id`.
**Java not found** — install Java 17+ (see Install step 1).
**QR code expired (Path B)** — QR codes expire in ~30 seconds. Re-run the
link command to generate a new one.
**signal-cli binary location** — The native Linux tarball extracts directly to
`~/.local/signal-cli` (a single file, not a directory). The system `aws` or
other tools named `signal-cli` won't be in PATH by default; check
`~/.local/bin/signal-cli`.

View File

@@ -5,20 +5,116 @@ description: Add Signal channel integration via signal-cli TCP daemon. Native ad
# Add Signal Channel
Adds Signal messaging support via a native adapter that speaks JSON-RPC to a [signal-cli](https://github.com/AsamK/signal-cli) TCP daemon. No Chat SDK bridge, no npm deps — only Node.js builtins.
Adds Signal messaging support via a native adapter that speaks JSON-RPC to a [signal-cli](https://github.com/AsamK/signal-cli) TCP daemon. No Chat SDK bridge — only Node.js builtins (`node:net`, `node:child_process`, `node:fs`).
Unlike Telegram or Discord, Signal has no bot API. NanoClaw registers as a full Signal account on a dedicated phone number (recommended) or links as a secondary device on your existing number.
## Prerequisites
`signal-cli` installed and a Signal account linked:
### Java
- macOS: `brew install signal-cli`
- Linux: download from [GitHub releases](https://github.com/AsamK/signal-cli/releases)
- Link your account: `signal-cli -a +1YOURNUMBER link` (follow the QR instructions)
signal-cli requires Java 17+:
```bash
java -version
```
If missing:
- **macOS:** `brew install --cask temurin@17`
- **Debian/Ubuntu:** `sudo apt-get install -y default-jre`
- **RHEL/Fedora:** `sudo dnf install -y java-17-openjdk`
Java 1725 all work.
### signal-cli
- **macOS:** `brew install signal-cli`
- **Linux:** download the native binary from [GitHub releases](https://github.com/AsamK/signal-cli/releases):
```bash
SIGNAL_CLI_VERSION=$(curl -fsSL https://api.github.com/repos/AsamK/signal-cli/releases/latest | python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'][1:])")
curl -fsSL "https://github.com/AsamK/signal-cli/releases/download/v${SIGNAL_CLI_VERSION}/signal-cli-${SIGNAL_CLI_VERSION}-Linux-native.tar.gz" \
| tar -xz -C ~/.local
ln -sf ~/.local/signal-cli ~/.local/bin/signal-cli
signal-cli --version
```
> The Linux native tarball extracts a single binary directly to `~/.local/signal-cli` (not into a subdirectory). The symlink above puts it on PATH.
## Registration
Two paths. The new-number path is recommended and battle-tested.
### Path A: Register a new number (recommended)
Use a dedicated SIM or VoIP number. NanoClaw owns it entirely.
> **VoIP numbers:** Signal requires SMS verification before voice. Some VoIP providers are blocked even for voice calls. If registration fails with an auth error, try a different provider or a physical SIM.
**Step 1: Solve the CAPTCHA**
Signal requires a CAPTCHA on first registration:
1. Open `https://signalcaptchas.org/registration/generate.html` in a browser
2. Solve the captcha
3. Right-click the **"Open Signal"** button → **Copy Link**
4. The link starts with `signalcaptcha://` — the token is everything after that prefix
**Step 2: Request SMS verification**
```bash
signal-cli -a +1YOURNUMBER register --captcha "PASTE_TOKEN_HERE"
```
**Step 3: Voice call fallback (if your number can't receive SMS)**
Wait ~60 seconds after the SMS request, then:
```bash
signal-cli -a +1YOURNUMBER register --voice --captcha "SAME_TOKEN"
```
Signal calls your number and reads a 6-digit code. The same captcha token is reusable — no need to solve a new one.
> You must request SMS first. Requesting voice immediately fails with `Invalid verification method: Before requesting voice verification…`
**Step 4: Verify**
```bash
signal-cli -a +1YOURNUMBER verify CODE
```
No output = success.
**Step 5: Set profile name (optional)**
> ⚠ Stop NanoClaw before running signal-cli commands — the daemon holds an exclusive lock on its data directory while running.
```bash
# macOS
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
signal-cli -a +1YOURNUMBER updateProfile --name "YourBotName"
# optionally: --avatar /path/to/avatar.jpg
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
# Linux
systemctl --user stop nanoclaw
signal-cli -a +1YOURNUMBER updateProfile --name "YourBotName"
systemctl --user start nanoclaw
```
### Path B: Link as secondary device
Joins an existing Signal account as a secondary device. Simpler, but NanoClaw shares your personal number.
```bash
signal-cli -a +1YOURNUMBER link --name "NanoClaw"
```
This prints a `tsdevice:` URI. Scan it as a QR code on your phone: **Settings → Linked Devices → Link New Device**. QR codes expire in ~30 seconds — re-run if it expires.
## Install
NanoClaw doesn't ship channels in trunk. This skill copies the Signal adapter and its tests in from the `channels` branch.
### Pre-flight (idempotent)
Skip to **Credentials** if all of these are already in place:
@@ -55,7 +151,7 @@ import './signal.js';
pnpm run build
```
No npm packages to install — the adapter uses only Node.js builtins (`node:net`, `node:child_process`, `node:fs`).
No npm packages to install — the adapter uses only Node.js builtins.
## Credentials
@@ -97,27 +193,73 @@ launchctl kickstart -k gui/$(id -u)/com.nanoclaw
systemctl --user restart nanoclaw
```
## Wiring
### DMs
After the service starts, send any message to the Signal number from your personal Signal app. The router auto-creates a `messaging_groups` row. Then:
```bash
sqlite3 data/v2.db \
"SELECT id, platform_id FROM messaging_groups WHERE channel_type='signal' ORDER BY created_at DESC LIMIT 5"
```
Pass the `id` to `/init-first-agent` or `/manage-channels` to wire it to an agent group.
### Groups
Add the Signal number to a group from your phone, send any message, then wire the resulting row the same way. For isolated per-group sessions:
```bash
NOW=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
sqlite3 data/v2.db "
INSERT OR IGNORE INTO messaging_group_agents
(id, messaging_group_id, agent_group_id, session_mode, priority, created_at)
VALUES
('mga-'||hex(randomblob(8)), 'mg-GROUPID', 'ag-AGENTID', 'isolated', 0, '$NOW');
"
```
### Grant user access
New Signal users (including the owner's Signal identity) are silently dropped with `not_member` until granted access. After the user's first message appears in `messaging_groups`:
```bash
NOW=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
sqlite3 data/v2.db "
INSERT OR REPLACE INTO user_roles (user_id, role, agent_group_id, granted_by, granted_at)
VALUES ('signal:UUID', 'owner', NULL, 'system', '$NOW');
INSERT OR IGNORE INTO agent_group_members (user_id, agent_group_id, added_by, added_at)
VALUES ('signal:UUID', 'ag-AGENTID', 'system', '$NOW');
"
```
Find the UUID from `messaging_groups.platform_id` or the `users` table.
## Next Steps
If you're in the middle of `/setup`, return to the setup flow now.
Otherwise, run `/init-first-agent` to create an agent and wire it to your Signal DM, or `/manage-channels` to wire this channel to an existing agent group. Signal is direct-addressable — your phone number is the platform ID.
Otherwise, run `/init-first-agent` to create an agent and wire it to your Signal DM, or `/manage-channels` to wire this channel to an existing agent group.
## Channel Info
- **type**: `signal`
- **terminology**: Signal has "chats" (1:1 DMs) and "groups."
- **how-to-find-id**: DMs use your phone number (e.g. `+15555550123`). Groups use `group:<groupId>` — find group IDs via `signal-cli -a +1YOURNUMBER listGroups`.
- **terminology**: Signal has "chats" (1:1 DMs) and "groups"
- **supports-threads**: no
- **platform-id-format**:
- DM: `signal:{UUID}` — sender's Signal UUID (ACI), **not** their phone number
- Group: `signal:{base64GroupId}` — base64-encoded GroupV2 ID
- **how-to-find-id**: Send a message to the bot, then query `messaging_groups` as shown above
- **typical-use**: Personal assistant via Signal DMs or small group chats
- **default-isolation**: One agent per Signal account. Multiple chats with the same operator can share an agent group; groups with other people should typically be separate.
- **default-isolation**: One agent per Signal account. Multiple chats with the same operator can share an agent group; groups with other people should typically use `isolated` session mode
### Features
- Markdown formatting — `**bold**`, `*italic*` / `_italic_`, `` `code` ``, ` ```code fence``` `, `~~strike~~`, `||spoiler||` (converted to Signal's offset-based text styles)
- Quoted replies — `replyTo*` fields populated from Signal quotes
- Typing indicators — DMs only (Signal doesn't support group typing)
- Echo suppression — outbound messages are matched on `(platformId, text)` within a 10 s TTL to avoid syncMessage loops
- Echo suppression — outbound messages matched on `(platformId, text)` within a 10 s TTL to avoid syncMessage loops
- Note to Self — messages you send to your own account from another device route to the agent as inbound with `isFromMe: true`
- Voice attachments — detected but not transcribed by default; the agent receives `[Voice Message]` placeholder text. Run `/add-voice-transcription` for local transcription via parakeet-mlx
@@ -145,4 +287,32 @@ If you see `Signal daemon not reachable at 127.0.0.1:7583` and `SIGNAL_MANAGE_DA
### Lost connection mid-session
If you see `Signal channel lost TCP connection to signal-cli daemon` in the logs, the daemon dropped us. There's no auto-reconnect yet — restart the service to re-establish.
If you see `Signal channel lost TCP connection to signal-cli daemon` in the logs, the daemon dropped the connection. Restart the service to re-establish.
### Messages dropped with `not_member`
The Signal user hasn't been granted membership. See "Grant user access" above. This affects every new Signal user, including the owner's Signal identity — which is a separate user record from their identity on other channels even if it's the same person.
### Captcha required
Signal requires a captcha for new registrations. Go to `https://signalcaptchas.org/registration/generate.html`, solve it, right-click "Open Signal", copy the link, extract the token after `signalcaptcha://`.
### `Invalid verification method: Before requesting voice verification…`
You must request SMS first, wait ~60 seconds, then request voice. Both steps can use the same captcha token.
### Config file in use / daemon lock
signal-cli holds an exclusive lock on its data directory while the daemon is running. Stop NanoClaw before running any `signal-cli` commands directly, then restart afterward.
### Group replies going to DM instead of group
Modern Signal groups use GroupV2. The adapter must extract the group ID from `envelope?.dataMessage?.groupV2?.id` — not `groupInfo?.groupId`, which is GroupV1/legacy. If group messages are routing as DMs, check `src/channels/signal.ts` and confirm the groupId extraction falls through to `groupV2.id`.
### Java not found
Install Java 17+ — see the Prerequisites section above.
### QR code expired (Path B)
QR codes expire in ~30 seconds. Re-run the link command to generate a new one.