setup:auto's Slack flow now asks the delivery mode up front (brightSelect),
then collects only the credential that mode needs:
- Socket Mode → SLACK_APP_TOKEN (xapp-…); skips the public-URL checklist
- Webhook → SLACK_SIGNING_SECRET + the public Request URL checklist (unchanged)
add-slack.sh now requires either SLACK_APP_TOKEN or SLACK_SIGNING_SECRET (was:
signing secret mandatory) and upserts whichever is present. The adapter already
selects the mode from SLACK_APP_TOKEN's presence (this PR) — this is the
guided-setup surface for it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Set SLACK_APP_TOKEN (xapp-…) and the adapter connects over an outbound
WebSocket (mode: 'socket') instead of an inbound HTTPS webhook — no public
endpoint required. Without the app token, behavior is unchanged (webhook mode);
the signing secret becomes optional under Socket Mode.
Requires @chat-adapter/slack@4.29.0, where Socket Mode is implemented — that pin
landed in the chat-SDK 4.29.0 bump this stacks on. SKILL.md documents the
app-level token (connections:write), enabling Socket Mode, and the
no-public-URL path.
Verified: this exact adapter change builds + passes the slack registration test
on current main at chat@4.29.0 / @chat-adapter/slack@4.29.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Companion to the core `chat` 4.29.0 bump on main. Bumps every channel adapter
install pin (8 /add-* SKILL.md + the setup/*.sh install scripts) and the
package.json deps to the matched 4.29.0 release, so `/add-<channel>` installs an
adapter whose ChatInstance matches main's Chat SDK bridge.
`chat` and @chat-adapter/* are version-locked, so all are pinned exactly to
4.29.0. The Slack adapter is proven on current main at chat@4.29.0 (build +
registration test green). Pre-existing standalone build/test failures on this
branch (optional native-adapter packages not installed, branch behind main) are
unchanged by this bump — identical failure set at the 4.26.0 baseline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
discord, gchat, github, imessage, linear, matrix, resend, teams, telegram,
webex, whatsapp-cloud, signal, wechat, whatsapp, emacs.
Same behavior shape as the slack/deltachat exemplars: import the real
src/channels/index.ts barrel and assert getRegisteredChannelNames() contains
the channel. Red if the barrel import line is deleted/drifts, if the barrel
fails to evaluate, or (for channels with an npm adapter) if the adapter package
is not installed — so each test also implicitly guards the skill's dependency.
signal and emacs have no npm adapter (signal-cli binary / http builtin), so
their tests guard the single barrel reach-in only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the structural barrel-parse with a behavior test that imports the real
barrel and asserts the registry actually contains the channel. This reflects
host boot: it goes red if the `import './<ch>.js';` line is deleted, if the
barrel fails to evaluate (channel genuinely won't register), or if the adapter
package isn't installed (the unmocked import throws) — so it also implicitly
verifies the dependency-install integration point. A structural check would
falsely pass in the latter two cases.
Importing is safe: registration is a pure top-level call; deltachat instantiates
DeltaChatOverJsonRpc only in setup(), and slack builds its SDK adapter/bridge
only in its factory — neither at import. Requires the adapter package installed,
which holds in the composed install (the skill's pnpm install runs first).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Guards the single reach-in a slack install makes into core — the
`import './slack.js';` line in the src/channels/index.ts barrel that triggers
the adapter's top-level registerChannelAdapter call. Structural (parses the
barrel) rather than behavior, so it stays hermetic and does not pull
@chat-adapter/slack into the test process; the adapter's createChatSdkBridge
core-API consumption is a typed call guarded by the build leg. Red-on-delete
of the barrel line. Template for the Chat SDK channel family.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Guards the single reach-in a deltachat install makes into core — the
`import './deltachat.js';` line in the src/channels/index.ts barrel that
triggers the adapter's top-level registerChannelAdapter call. Structural
(parses the barrel) rather than behavior, so it stays hermetic and does not
pull the native @deltachat/stdio-rpc-server into the test process; the build
leg covers that the import resolves. Red-on-delete of the barrel line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The auth-cleanup branch ran on every `shouldReconnect=false` path, which
is true for *both* `DisconnectReason.loggedOut` *and* `shuttingDown=true`.
That meant every clean `systemctl restart nanoclaw` would log "WhatsApp
logged out" and `fs.rmSync(authDir)` — wiping a perfectly good session
and forcing a fresh re-pair on next start.
Split the branch: only clear `authDir` when the disconnect reason is
actually `DisconnectReason.loggedOut`. On clean shutdown, log
"WhatsApp adapter stopped (auth preserved)" and exit cleanly. The next
process boot will pick up the preserved auth via
`useMultiFileAuthState` and reconnect transparently.
Symptom in logs that this fixes:
WhatsApp connection closed reason=undefined shuttingDown=true
WhatsApp logged out
WhatsApp auth cleared (wrong — this was a clean shutdown)
... next restart ...
WhatsApp pairing code: XXXXXXXX (because auth is gone)
This pair of bugs together (the previous commit + this one) is why
WhatsApp installs with `WHATSAPP_PHONE_NUMBER` set on Baileys 7.x feel
unstable: any restart wipes auth, then the next restart's adapter
self-destructs while trying to re-pair, leaving the bot offline until
a human re-runs the pair flow. With both fixes a `systemctl restart`
is now a transparent ~3s connection blip.
Baileys 7.x does not reliably flip `state.creds.registered` back to
true after the post-pair stream-restart (statusCode 515). The adapter
then sees `registered=false` on an already-paired socket and queues a
fresh `requestPairingCode()` 3 seconds later. The WhatsApp server sees
two conflicting auth flows for the same account, rejects with 401, and
the adapter wipes `authDir` — every restart forces a re-pair from
scratch.
Use `state.creds.me` instead. It is set during QR / pair-code handshake
and is the authoritative "this socket has an account" signal — it does
not toggle on stream restarts.
Symptom in logs that this fixes:
Connected to WhatsApp
WhatsApp pairing code: XXXXXXXX (3s later — the bug)
WhatsApp connection closed reason=401
WhatsApp logged out
WhatsApp auth cleared
Reproduced on Baileys 7.0.0-rc.9. Affects any install with
WHATSAPP_PHONE_NUMBER set, which is the recommended config for
dedicated bot numbers.
Combine both test suites — inbound bot mention detection (#2560 from
channels) and outbound parseWhatsAppMentions (this PR).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When WhatsApp issues a forced logout (status 401), the adapter now
deletes store/auth/ immediately rather than leaving stale credentials
on disk. Previously, stale credentials persisted across service
restarts — the next startup would re-attempt authentication with the
dead session, receive a second 401, and contribute to WhatsApp's
temporary re-link cooldown ("can't link new devices now. try again
later").
After clearing, a log message instructs the operator to set
WHATSAPP_ENABLED=true and restart in order to re-link. Without that
env var, the adapter stays dormant on subsequent restarts (existing
guard at adapter startup: no creds + no WHATSAPP_ENABLED → return
null), avoiding any further auth attempts until the operator is ready.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Before this change, the inbound construction site hard-coded
`isMention: !isGroup ? true : undefined`, which meant group messages
that explicitly @-mentioned the bot never set the field. The router
then never woke the agent on a mention-only trigger.
Detection lives in a new pure helper `isBotMentionedInGroup` which
scans `contextInfo.mentionedJid` across the four message types that
can host mentions (extendedTextMessage + image/video/document
captions), matching against both the bot's phone JID and LID since
modern WhatsApp clients increasingly emit the LID for phone-number
mentions. A second helper `computeIsMention` wraps the DM/group
ternary so both branches of the fix are unit-testable.
Tests in src/channels/whatsapp.test.ts cover phone-JID detection,
LID-only detection, image-caption mentions, the negative cases, and
the call-site isMention semantics for DMs vs groups vs no-mention.
Fixes#2560
Two bugs in src/channels/whatsapp.ts that compound each other:
1) Outbound `@<digits>` never rendered as a WhatsApp tag because every
send path called `sock.sendMessage(jid, { text })` with no `mentions`
array. Baileys copies `mentions` into `contextInfo.mentionedJid`
(lib/Utils/messages.js:477), and WhatsApp clients use that list to
draw the bold/clickable tag. Without it the `@<digits>` is plain
text with no notification.
Fix: new `parseWhatsAppMentions` scans outbound text for
`@<5-15 digits>` (with optional leading `+`, stripped so the literal
matches the JID), and `formatWhatsApp` now returns `{ text, mentions }`.
All four outbound paths — `sendRawMessage`, `flushOutgoingQueue`,
media caption, and the normal-text branch in `deliver` — pass the
`mentions` array through. Code-block protected regions are exempt
so phone-like sequences inside fenced code aren't tagged.
2) On SIGTERM the `connection.update` close handler unconditionally
called `connectSocket()` (because `shouldReconnect` was only false
for `loggedOut`). The fresh `useMultiFileAuthState` it initialized
could truncate `creds.json` mid-write as the process exited, leaving
a 0-byte creds file and forcing a fresh QR pairing on next start.
Fix: new `shuttingDown` flag set by `teardown()`; the close handler
skips the reconnect when shutting down.
Adds src/channels/whatsapp.test.ts covering the new mention parser
(10 cases: basic extraction, `+` stripping, multiple/dedup mentions,
edge cases like emails, short sequences, parens, punctuation).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The blanket `if (fromMe) continue` filter dropped all messages from the
linked device, including user-typed messages in self-chat. Use the
existing sentMessageCache to distinguish bot echoes from user messages
when chatJid matches the bot's own phone JID.
Introduced in c02ac06 (Apr 14 2026).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Never fall back to Baileys' hardcoded stale version — it will just
get rejected with 405 at the Noise layer. Throw a clear error
instead so the problem is visible immediately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Baileys' fetchLatestWaWebVersion scrapes sw.js which WhatsApp
aggressively rate-limits (429). When it fails, Baileys falls back
to a hardcoded version (2.3000.1027934701) that goes stale within
weeks — WhatsApp rejects connections with a mismatched buildHash
(405 at Noise protocol layer, before QR/pairing code can start).
Add resolveWaWebVersion() that fetches the current version from
wppconnect.io/whatsapp-versions first, then falls back to Baileys'
own fetch, then to the hardcoded default. Applied to both the
adapter and whatsapp-auth.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v1 ran on Baileys 7.0.0-rc.9 which has proper LID support via
extractAddressingContext (participantAlt/remoteJidAlt on every inbound
message) and signalRepository.lidMapping.getPNForLID. The v2 adapter
was mistakenly downgraded to v6 where getPNForLID doesn't exist,
making the call dead code behind an `as any` cast. This caused:
- Unresolvable LIDs leaking to the router as @lid platform IDs
- Split sessions when dual messaging_groups rows were created
- Silent message drops on cold start before mappings were learned
Changes:
- Pin @whiskeysockets/baileys to exact 7.0.0-rc.9 (last release)
- Import proto directly (ESM named export in v7, no createRequire hack)
- Remove getPlatformId monkey-patch (bug fixed in v7)
- translateJid: use msg.key.remoteJidAlt/participantAlt first, then
real signalRepository.lidMapping.getPNForLID (no as any cast)
- Replace chats.phoneNumberShare with lid-mapping.update event
- proto.Message.fromObject → proto.Message.create (v7 migration)
- Resolve sender JID in groups via participantAlt
- Mark DM inbound messages with isMention=true (subsumes #2213 fix)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous wording promised a welcome DM "shortly", but in practice the
welcome can be delayed (cold container start, OneCLI selective-secret
mode, etc.) until well after the user has more terminal interactions to
complete. Telling them to wait in Telegram pulls attention away from the
installer at the worst moment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Enables the channel-approval flow to show the Telegram group name
in the approval card instead of a generic "a telegram channel".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR #1900 added the splitForLimit helper and maxTextLength config option,
but explicitly flagged that channel adapters need a follow-up to wire it.
This commit completes the wiring for Telegram.
Without this, the telegram adapter silently truncates outbound messages
>4096 chars via legacy truncateMessage() behavior. With this fix, the
splitter engages and posts chunks sequentially. The returned id is the
first chunk's id so edits/reactions still target the reply head.
Adds resolveChannelName to the Slack adapter so the channel-approval
flow can show the actual channel name in the approval card. Uses the
existing fetchThread → conversations.info path.
Depends on: qwibitai/nanoclaw#2105 (adds resolveChannelName to ChannelAdapter interface)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bring channels up to date with main, including the channel-inbound
attachment path-traversal fix.
Resolved conflicts:
- package.json: kept channels-specific deps + added @clack/core from main
- pnpm-lock.yaml: regenerated against merged package.json
- setup/index.ts: union STEPS map (pair-telegram, whatsapp-auth, signal-auth)
- setup/whatsapp-auth.ts: took main's version (deliberate fork for setup-auto)
The native Signal adapter previously logged-and-dropped any
OutboundFile entries with a TODO. This wires through to signal-cli's
already-supported `send` JSON-RPC `attachments` parameter:
- New `sendAttachments(platformId, files)` helper writes each
OutboundFile.data Buffer to a temp file in os.tmpdir(), passes
the paths to `tcp.rpc('send', { attachments: [...] })`, then
cleans up the temp files in finally{}.
- `deliver()` no longer drops files — sends accompanying text
first via the existing sendText (preserving chunking + textStyle),
then attachments as a single send call.
- Filename sanitization replaces `/`, `\`, and `\0` with `_` so
operator-supplied filenames can't escape tmpdir (CWE-22).
- Tests cover: single attachment, text+attachment ordering,
multiple attachments in one send, group destinations, and a
structural invariant proving sanitized paths stay inside tmpdir().
Total signal tests: 33 → 37 (one stale "drops files" test replaced
by the positive-behavior tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slipped through during the #2035 rebase resolution — both #2030's import
and ours landed in the merge. TypeScript dedups by symbol so it didn't
fail the typecheck, but it's noise and would've eventually tripped a
linter rule.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original approach passed ANTHROPIC_AUTH_TOKEN into the container
as an env var and disabled the proxy for the custom host (NO_PROXY) —
which works, but bypasses OneCLI entirely for that credential. The
container holds the raw secret, the gateway loses audit/rotation, and
we lose the rest of the vault's protections for this cohort.
OneCLI-native version: store the token as a generic secret with header
injection (--header-name Authorization --value-format 'Bearer {value}'
+ host-pattern matching the base URL hostname). The container only
needs ANTHROPIC_BASE_URL plus a placeholder ANTHROPIC_AUTH_TOKEN — the
proxy rewrites the Authorization header on the wire.
setup/lib/setup-config.ts — adds --anthropic-auth-token alongside the
existing --anthropic-base-url.
setup/auto.ts — runAuthStep short-circuits the auth-method prompt when
both NANOCLAW_ANTHROPIC_BASE_URL and NANOCLAW_ANTHROPIC_AUTH_TOKEN are
set: creates the OneCLI generic secret, writes ANTHROPIC_BASE_URL to
.env (so the runtime reads it), and appends `import './claude.js';` to
src/providers/index.ts (so the provider only registers when the user
has configured a custom endpoint — no branching for everyone else).
src/providers/claude.ts — drops ANTHROPIC_AUTH_TOKEN/NO_PROXY
passthrough. Reads ANTHROPIC_BASE_URL from .env, sets a placeholder
ANTHROPIC_AUTH_TOKEN in container env so the SDK includes an
Authorization header for OneCLI to overwrite.
src/providers/index.ts — removes the unconditional import; setup
appends it on demand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Users with a custom Anthropic-compatible endpoint (ANTHROPIC_BASE_URL) were
getting 401s because the OneCLI proxy injects ANTHROPIC_API_KEY=placeholder
and forwards to api.anthropic.com, overriding the custom endpoint and key.
Add a claude provider host config that reads ANTHROPIC_BASE_URL,
ANTHROPIC_AUTH_TOKEN, and CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC from .env
and passes them into the container. Also sets NO_PROXY for the custom host so
the OneCLI proxy doesn't intercept those requests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Matches the OneCLI CLI's own format expectation ("oc_... format" per
`onecli auth login --help`) so a malformed token gets caught at setup
time rather than at first vault call.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the example.internal placeholder with the hosted gateway URL
so the advanced screen and --help suggest the canonical destination
out of the box.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>