Add @relaycast/openclaw package for OpenClaw integration#3
Conversation
Introduce a new package that makes it dead simple for OpenClaw developers to add structured multi-claw messaging via Relaycast. Includes: - OpenClawBridge class: connects any claw instance to a Relaycast workspace, auto-registering as an agent with channel join, DMs, threads, reactions, search, and real-time WebSocket events - Skill installer: `relay openclaw setup` writes a SKILL.md and .env into ~/.openclaw/workspace/relaycast/ and optionally patches openclaw.json with the MCP server config - Config detection: auto-discovers OpenClaw installation and parses config - CLI integration: `relay openclaw setup` and `relay openclaw status` - 19 tests covering bridge, config detection, and skill setup https://claude.ai/code/session_01K6SpufavLtoKCvM64X8QG4
Adds a dedicated section showcasing the OpenClaw integration with CLI and SDK code examples, feature badges, and an OpenClaw entry in the "works with" tool grid plus nav link. https://claude.ai/code/session_01K6SpufavLtoKCvM64X8QG4
| const env = ENV_TEMPLATE | ||
| .replace('__API_KEY__', options.apiKey) | ||
| .replace('__CLAW_NAME__', options.clawName ?? 'my-claw') | ||
| .replace('__BASE_URL__', options.baseUrl ?? 'https://api.relaycast.dev'); |
There was a problem hiding this comment.
🟡 Sequential String.replace() on ENV_TEMPLATE allows user input to corrupt .env output
The chained .replace() calls on lines 144-147 use simple string replacement, which means if an earlier replacement value contains a later placeholder token, the later .replace() will match and substitute within the already-replaced content, corrupting the .env file.
Detailed Explanation
The replacement chain is:
ENV_TEMPLATE
.replace('__API_KEY__', options.apiKey)
.replace('__CLAW_NAME__', options.clawName ?? 'my-claw')
.replace('__BASE_URL__', options.baseUrl ?? 'https://api.relaycast.dev');If options.apiKey contains the literal string __CLAW_NAME__, the second .replace() will match it inside the already-substituted API key value. For example:
- API key:
rk_test___CLAW_NAME__→ after first replace:RELAY_API_KEY=rk_test___CLAW_NAME__ - Second replace matches
__CLAW_NAME__inside the API key →RELAY_API_KEY=rk_test_my-clawandRELAY_CLAW_NAME=__CLAW_NAME__is left unreplaced
Similarly, if clawName contains __BASE_URL__, the third replace corrupts the claw name and leaves RELAY_BASE_URL with the raw placeholder.
While API keys are unlikely to contain these tokens, clawName is user-provided (via CLI --name flag at packages/cli/src/commands/openclaw.ts:29) and could theoretically contain __BASE_URL__.
Impact: The generated .env file would have incorrect values, causing the Relaycast skill to malfunction (wrong API key, wrong claw name, or missing base URL).
| const env = ENV_TEMPLATE | |
| .replace('__API_KEY__', options.apiKey) | |
| .replace('__CLAW_NAME__', options.clawName ?? 'my-claw') | |
| .replace('__BASE_URL__', options.baseUrl ?? 'https://api.relaycast.dev'); | |
| const env = [ | |
| '# Relaycast configuration for this OpenClaw skill', | |
| `RELAY_API_KEY=${options.apiKey}`, | |
| `RELAY_CLAW_NAME=${options.clawName ?? 'my-claw'}`, | |
| `RELAY_BASE_URL=${options.baseUrl ?? 'https://api.relaycast.dev'}`, | |
| '', | |
| ].join('\n'); |
Was this helpful? React with 👍 or 👎 to provide feedback.
| store.setState({ connectionStatus: 'disconnected' }); | ||
| ws.disconnect(); | ||
| }; | ||
| }, [clients, store, channels]); |
There was a problem hiding this comment.
🔴 WebSocket tears down and reconnects on every parent re-render due to unstable channels array reference
The useEffect in RelayProvider includes channels in its dependency array. Because React compares dependencies with Object.is, passing an array literal like channels={['general', 'alerts']} creates a new reference on every parent render, causing the effect to re-run. Each re-run disconnects the WebSocket (ws.disconnect()), then reconnects (ws.connect()), losing all in-flight subscriptions and events.
Root Cause and Impact
At packages/react/src/provider.tsx:58, channels is listed in the dependency array:
}, [clients, store, channels]);Unlike clients (stabilized via useMemo) and store (created once), channels is a raw prop. A typical consumer writes:
<RelayProvider apiKey="..." agentToken="..." channels={['general']}>Every render of the parent creates a new ['general'] array, so Object.is(prevChannels, nextChannels) is false. The cleanup runs (lines 51-57), disconnecting the WebSocket and setting status to 'disconnected', then immediately re-runs the setup, reconnecting. This causes connection thrashing, dropped real-time events, and unnecessary network overhead on every unrelated parent re-render.
Prompt for agents
In packages/react/src/provider.tsx, the `channels` prop is an array that will have a new reference on every parent render if passed as a literal. This causes the WebSocket useEffect to tear down and re-run on every parent re-render. Fix this by either: (1) removing `channels` from the useEffect dependency array and instead using a ref to track the latest channels value, sending subscribe/unsubscribe only when channels actually change (via a separate useEffect with a JSON.stringify or deep comparison), or (2) memoizing the channels internally using a custom hook like: `const stableChannels = useRef(channels); stableChannels.current = channels;` and only including stable deps in the main useEffect, while handling channel subscription changes in a separate effect.
Was this helpful? React with 👍 or 👎 to provide feedback.
| useEffect(() => { | ||
| if (!state.loading) return; | ||
| ctx.agent.dms.conversations() | ||
| .then((data) => { | ||
| store.setState({ dms: { data, loading: false, error: null } }); | ||
| }) | ||
| .catch(() => { | ||
| // Silently fail on refetch — keep existing data | ||
| store.setState({ dms: { ...store.getState().dms, loading: false } }); | ||
| }); | ||
| }, [state.loading, ctx.agent, store]); |
There was a problem hiding this comment.
🟡 useDMs double-fetches on mount because the first effect's loading: true triggers the second refetch effect
The useDMs hook has two useEffects that both fire API requests on initial mount, resulting in two concurrent calls to ctx.agent.dms.conversations().
Root Cause and Impact
The first effect (line 10) synchronously sets loading: true and starts a fetch. The second effect (line 34) watches state.loading and fires a refetch whenever it becomes true — intended only for DM event notifications via handleDmReceived (packages/react/src/reducer.ts:306-310).
On mount:
- First
useEffect(line 10-26): callsstore.setState({ dms: { ...dms, loading: true } })then fires fetch A. - React re-renders because
useSyncExternalStoredetectsdmschanged. - Second
useEffect(line 34-44): seesstate.loading === trueand fires fetch B. - Both fetch A and fetch B are now in-flight concurrently.
This causes a race condition where both responses overwrite each other, and the API is called twice unnecessarily on every mount. If the API is rate-limited or slow, this can cause visible UI flicker (loading → data → loading → data) and wasted network requests.
Prompt for agents
In packages/react/src/hooks/useDMs.ts, the second useEffect (lines 34-44) is intended to refetch DMs when a dm.received event sets loading to true, but it also fires on the initial mount because the first useEffect also sets loading to true. Fix this by adding a ref to skip the initial trigger, e.g.:
const mountedRef = useRef(false);
useEffect(() => {
if (!mountedRef.current) { mountedRef.current = true; return; }
if (!state.loading) return;
// ... refetch logic ...
}, [state.loading, ctx.agent, store]);
Alternatively, use a distinct flag (like a 'stale' boolean) instead of reusing 'loading' for both initial load and event-driven refetches.
Was this helpful? React with 👍 or 👎 to provide feedback.
Introduce a new package that makes it dead simple for OpenClaw developers
to add structured multi-claw messaging via Relaycast. Includes:
auto-registering as an agent with channel join, DMs, threads, reactions,
search, and real-time WebSocket events
relay openclaw setupwrites a SKILL.md and .env into~/.openclaw/workspace/relaycast/ and optionally patches openclaw.json
with the MCP server config
relay openclaw setupandrelay openclaw statushttps://claude.ai/code/session_01K6SpufavLtoKCvM64X8QG4