From b6be3b9bf458e4710c0a8537002a60b6486447cb Mon Sep 17 00:00:00 2001 From: Ira Abramov Date: Sat, 25 Apr 2026 16:52:20 +0300 Subject: [PATCH] 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 --- .claude/skills/add-signal-v2/SKILL.md | 311 -------------------------- .claude/skills/add-signal/SKILL.md | 198 ++++++++++++++-- 2 files changed, 184 insertions(+), 325 deletions(-) delete mode 100644 .claude/skills/add-signal-v2/SKILL.md diff --git a/.claude/skills/add-signal-v2/SKILL.md b/.claude/skills/add-signal-v2/SKILL.md deleted file mode 100644 index 06edd17..0000000 --- a/.claude/skills/add-signal-v2/SKILL.md +++ /dev/null @@ -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 17–25 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`. diff --git a/.claude/skills/add-signal/SKILL.md b/.claude/skills/add-signal/SKILL.md index e6d41aa..7dcc8ad 100644 --- a/.claude/skills/add-signal/SKILL.md +++ b/.claude/skills/add-signal/SKILL.md @@ -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 17–25 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:` — 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.