chrome-openclaw-sider 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,98 +1,65 @@
1
- # chrome-openclaw-sider
1
+ # Chrome Channel for OpenClaw in Sider
2
2
 
3
- Official OpenClaw channel plugin for connecting OpenClaw in Sider.
3
+ The official Chrome channel plugin for connecting OpenClaw to the Sider Chrome extension.
4
4
 
5
- It supports the standard OpenClaw channel login flow, one-time setup token exchange, and direct relay-token configuration.
5
+ Once connected, you can control OpenClaw directly from the Sider Chrome sidebar.
6
6
 
7
- ## Compatibility
7
+ ## 【【【 Quick install from the terminal 】】】
8
8
 
9
- - `openclaw >= 2026.3.22`
10
-
11
- ## Recommended Install
12
-
13
- For most users, use the companion CLI:
9
+ Run in Terminal:
14
10
 
11
+ ================================
15
12
  ```bash
16
13
  npx -y @sider-ai/chrome-openclaw-sider-cli install
17
14
  ```
15
+ ================================
18
16
 
19
- This installs or updates the plugin and starts terminal pairing automatically.
17
+ You can also paste this command into the OpenClaw chat box and ask OpenClaw to install it for you. This will install or update the plugin and automatically start terminal pairing.
20
18
 
21
- ## Manual Install
19
+ ## 【【【 Manual installation 】】】
22
20
 
23
- Install the plugin:
21
+ If the quick install does not work, follow these steps:
24
22
 
25
- ```bash
26
- openclaw plugins install chrome-openclaw-sider
27
- ```
23
+ ### 【 1. Install or update the plugin 】
28
24
 
29
- If the plugin is already installed, update it instead:
25
+ Install in Terminal:
30
26
 
27
+ ================================
31
28
  ```bash
32
- openclaw plugins update chrome-openclaw-sider
29
+ openclaw plugins install clawhub:chrome-openclaw-sider
33
30
  ```
31
+ ================================
34
32
 
35
- Start the pairing flow:
33
+ If the plugin is already installed, update it with:
36
34
 
35
+ ================================
37
36
  ```bash
38
- openclaw channels login --channel chrome-openclaw-sider
37
+ openclaw plugins update clawhub:chrome-openclaw-sider
39
38
  ```
39
+ ================================
40
40
 
41
- The terminal will show a short pairing code. Enter that code in the Sider browser extension, then the plugin writes the long-lived token back to `channels.chrome-openclaw-sider`.
42
-
43
- ## Configuration
41
+ You can run these commands directly in your terminal, or paste them into the OpenClaw chat box and ask OpenClaw to execute them.
44
42
 
45
- The plugin supports three common setup modes.
43
+ ### 【 2. Pair the plugin with Sider’s Chrome extension using a pairing code 】
46
44
 
47
- ### Terminal Pairing
48
-
49
- Use the built-in OpenClaw login flow:
45
+ Run in Terminal:
50
46
 
47
+ ================================
51
48
  ```bash
52
49
  openclaw channels login --channel chrome-openclaw-sider
53
50
  ```
51
+ ================================
54
52
 
55
- ### Setup Token
56
-
57
- Write a one-time token to `channels.chrome-openclaw-sider.setupToken`:
58
-
59
- ```json
60
- {
61
- "channels": {
62
- "chrome-openclaw-sider": {
63
- "enabled": true,
64
- "setupToken": "<one-time-token>"
65
- }
66
- }
67
- }
68
- ```
69
-
70
- On startup, the plugin exchanges `setupToken` for a long-lived `token`, writes it back to config, and removes `setupToken`.
71
-
72
- ### Direct Relay Token
53
+ Your terminal will display a short pairing code. Enter this code in the Sider browser extension.
73
54
 
74
- Write a long-lived relay token directly:
55
+ In the Sider Chrome extension, open the **Claw** widget from the right column of the extension and enter the pairing code there. This will connect the Sider Chrome extension to your OpenClaw instance.
75
56
 
76
- ```json
77
- {
78
- "channels": {
79
- "chrome-openclaw-sider": {
80
- "enabled": true,
81
- "token": "<relay-token>"
82
- }
83
- }
84
- }
85
- ```
86
-
87
- Named accounts can be configured under `channels.chrome-openclaw-sider.accounts.<id>`.
57
+ ### 【 3. Restart the gateway 】
88
58
 
89
- After changing config manually, restart the gateway:
59
+ After entering the pairing code, restart the gateway in Terminal:
90
60
 
61
+ ================================
91
62
  ```bash
92
63
  openclaw gateway restart
93
64
  ```
94
-
95
- ## Notes
96
-
97
- - `setupToken` and `token` should not be used together for the same account.
98
- - Pairing is the simplest setup path for most users.
65
+ ================================
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "chrome-openclaw-sider",
3
3
  "private": false,
4
- "version": "1.0.0",
4
+ "version": "1.0.2",
5
5
  "description": "Official Chrome channel plugin for connecting OpenClaw in Sider Chrome extension",
6
6
  "type": "module",
7
7
  "files": [
package/src/account.ts CHANGED
@@ -191,6 +191,7 @@ export const siderSetupWizard: ChannelSetupWizard = {
191
191
 
192
192
  const progress = prompter.progress("Waiting for connection...");
193
193
  const reportPendingUpdate = createSiderPairingPendingUpdateReporter({
194
+ pairingCode: pairing.pairingCode,
194
195
  report: (message) => {
195
196
  progress.update(message);
196
197
  },
@@ -199,6 +200,9 @@ export const siderSetupWizard: ChannelSetupWizard = {
199
200
  const paired = await waitForSiderPairing({
200
201
  pairing,
201
202
  onPending: reportPendingUpdate,
203
+ onRetryableError: (message) => {
204
+ progress.update(message);
205
+ },
202
206
  });
203
207
  progress.stop("Connected.");
204
208
 
package/src/auth.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  import { type OpenClawConfig, type PluginRuntime } from "openclaw/plugin-sdk";
2
2
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
3
3
  import { type OpenClawPluginService } from "openclaw/plugin-sdk/plugin-entry";
4
- import { SIDER_CHANNEL_ID, SIDER_DEFAULT_BASE_URL } from "./config.js";
4
+ import {
5
+ SIDER_CHANNEL_ID,
6
+ SIDER_DEFAULT_BASE_URL,
7
+ readDefaultSiderSetupTokenEnv,
8
+ readSiderBaseUrlEnv,
9
+ } from "./config.js";
5
10
  import { SIDER_USER_AGENT } from "./user-agent.js";
6
11
 
7
12
  export const SIDER_AUTH_EXCHANGE_API_PATH = "/v1/claws/register";
8
- export const SIDER_SETUP_TOKEN_ENV = "SIDER_SETUP_TOKEN";
9
- export const SIDER_BASE_URL_ENV = "SIDER_BASE_URL";
10
-
11
13
  export type SiderSetupConfigSnapshot = {
12
14
  enabled?: boolean;
13
15
  setupToken?: string;
@@ -51,12 +53,8 @@ function normalizeGatewayUrl(raw?: string): string | undefined {
51
53
  return trimmed ? trimmed.replace(/\/+$/, "") : undefined;
52
54
  }
53
55
 
54
- function normalizeAccountEnvSuffix(accountId: string): string {
55
- return accountId.trim().replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
56
- }
57
-
58
56
  export function resolveSiderBaseUrl(): string {
59
- return normalizeGatewayUrl(process.env[SIDER_BASE_URL_ENV]) ?? SIDER_DEFAULT_BASE_URL;
57
+ return normalizeGatewayUrl(readSiderBaseUrlEnv()) ?? SIDER_DEFAULT_BASE_URL;
60
58
  }
61
59
 
62
60
  function getPendingOperation(accountId: string): Promise<OpenClawConfig> | undefined {
@@ -232,15 +230,8 @@ export function resolveSiderSetupToken(
232
230
  if (configuredToken) {
233
231
  return configuredToken;
234
232
  }
235
- const accountSuffix = normalizeAccountEnvSuffix(accountId);
236
- if (accountSuffix) {
237
- const namedToken = trimMaybe(process.env[`${SIDER_SETUP_TOKEN_ENV}_${accountSuffix}`]);
238
- if (namedToken) {
239
- return namedToken;
240
- }
241
- }
242
233
  if (accountId === DEFAULT_ACCOUNT_ID) {
243
- return trimMaybe(process.env[SIDER_SETUP_TOKEN_ENV]);
234
+ return trimMaybe(readDefaultSiderSetupTokenEnv());
244
235
  }
245
236
  return undefined;
246
237
  }
package/src/channel.ts CHANGED
@@ -3817,6 +3817,7 @@ export const siderPlugin: ChannelPlugin<ResolvedSiderAccount> = {
3817
3817
  }),
3818
3818
  );
3819
3819
  const reportPendingUpdate = createSiderPairingPendingUpdateReporter({
3820
+ pairingCode: pairing.pairingCode,
3820
3821
  report: (message) => {
3821
3822
  runtime.log(message);
3822
3823
  },
@@ -3825,6 +3826,9 @@ export const siderPlugin: ChannelPlugin<ResolvedSiderAccount> = {
3825
3826
  const paired = await waitForSiderPairing({
3826
3827
  pairing,
3827
3828
  onPending: reportPendingUpdate,
3829
+ onRetryableError: (message) => {
3830
+ runtime.log(message);
3831
+ },
3828
3832
  });
3829
3833
  const latestCfg = loadConfig();
3830
3834
  const nextCfg = applySiderSetupAccountConfig({
package/src/config.ts CHANGED
@@ -12,3 +12,18 @@ export const SIDER_CHANNEL_BLURB = "Command your OpenClaw from Chrome Sidebar di
12
12
  export const SIDER_CHANNEL_ALIASES = ["sider"] as const;
13
13
 
14
14
  export const SIDER_DEFAULT_BASE_URL = "https://sider.ai/api/claw/self";
15
+ export const SIDER_SETUP_TOKEN_ENV = "SIDER_SETUP_TOKEN";
16
+ export const SIDER_BASE_URL_ENV = "SIDER_BASE_URL";
17
+ export const SIDER_REMOTE_BROWSER_ENABLE_ENV = "SIDER_ENABLE_REMOTE_BROWSER_MCP";
18
+
19
+ export function readSiderBaseUrlEnv(): string | undefined {
20
+ return process.env.SIDER_BASE_URL;
21
+ }
22
+
23
+ export function readDefaultSiderSetupTokenEnv(): string | undefined {
24
+ return process.env.SIDER_SETUP_TOKEN;
25
+ }
26
+
27
+ export function readSiderRemoteBrowserEnableEnv(): string | undefined {
28
+ return process.env.SIDER_ENABLE_REMOTE_BROWSER_MCP;
29
+ }
@@ -1,7 +1,11 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
- import { SIDER_CHANNEL_ID } from "./config.js";
2
+ import {
3
+ SIDER_CHANNEL_ID,
4
+ SIDER_REMOTE_BROWSER_ENABLE_ENV,
5
+ readSiderRemoteBrowserEnableEnv,
6
+ } from "./config.js";
3
7
 
4
- export const SIDER_REMOTE_BROWSER_ENABLE_ENV = "SIDER_ENABLE_REMOTE_BROWSER_MCP";
8
+ export { SIDER_REMOTE_BROWSER_ENABLE_ENV } from "./config.js";
5
9
 
6
10
  const SIDER_REMOTE_BROWSER_PREPEND_CONTEXT = [
7
11
  "Sider remote-browser mode is enabled.",
@@ -31,7 +35,7 @@ function isTruthyEnv(value: string | undefined): boolean {
31
35
  }
32
36
 
33
37
  function isRemoteBrowserSupportEnabled(): boolean {
34
- return isTruthyEnv(process.env[SIDER_REMOTE_BROWSER_ENABLE_ENV]);
38
+ return isTruthyEnv(readSiderRemoteBrowserEnableEnv());
35
39
  }
36
40
 
37
41
  function buildRemoteBrowserPromptInjection(): RemoteBrowserPromptInjection {
package/src/setup-core.ts CHANGED
@@ -60,6 +60,13 @@ export class SiderPairingExpiredError extends Error {
60
60
  }
61
61
  }
62
62
 
63
+ export class SiderPairingTransientError extends Error {
64
+ constructor(message: string) {
65
+ super(message);
66
+ this.name = "SiderPairingTransientError";
67
+ }
68
+ }
69
+
63
70
  function trimMaybe(value: unknown): string | undefined {
64
71
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
65
72
  }
@@ -134,6 +141,43 @@ function sleep(ms: number): Promise<void> {
134
141
  return new Promise((resolve) => setTimeout(resolve, ms));
135
142
  }
136
143
 
144
+ function describeUnknownError(error: unknown): string {
145
+ if (error instanceof Error && error.message) {
146
+ return error.message;
147
+ }
148
+ return String(error);
149
+ }
150
+
151
+ function isRetryableFetchError(error: unknown): boolean {
152
+ if (!(error instanceof Error)) {
153
+ return false;
154
+ }
155
+ if (error.name === "TypeError" && /fetch failed/i.test(error.message)) {
156
+ return true;
157
+ }
158
+
159
+ const cause = "cause" in error ? error.cause : undefined;
160
+ const causeCode =
161
+ cause && typeof cause === "object" && "code" in cause && typeof cause.code === "string"
162
+ ? cause.code
163
+ : undefined;
164
+ return Boolean(
165
+ causeCode &&
166
+ [
167
+ "ECONNRESET",
168
+ "ECONNREFUSED",
169
+ "EHOSTUNREACH",
170
+ "ENETUNREACH",
171
+ "ENOTFOUND",
172
+ "ETIMEDOUT",
173
+ "UND_ERR_CONNECT_TIMEOUT",
174
+ "UND_ERR_HEADERS_TIMEOUT",
175
+ "UND_ERR_BODY_TIMEOUT",
176
+ "UND_ERR_SOCKET",
177
+ ].includes(causeCode),
178
+ );
179
+ }
180
+
137
181
  function supportsTerminalFormatting(): boolean {
138
182
  return Boolean(process.stdout.isTTY);
139
183
  }
@@ -170,11 +214,24 @@ export function formatSiderPairingTtl(remainingMs: number): string {
170
214
  return `${minutes}:${String(seconds).padStart(2, "0")}`;
171
215
  }
172
216
 
173
- export function formatSiderPairingPendingMessage(remainingMs: number): string {
174
- return `Waiting for connection... Pairing code expires in ${formatSiderPairingTtl(remainingMs)}.`;
217
+ export function formatSiderPairingPendingMessage(params: {
218
+ pairingCode: string;
219
+ remainingMs: number;
220
+ }): string {
221
+ return `Waiting for connection... Pairing code: ${formatHighlightedPairingCode(params.pairingCode)}. Pairing code expires in ${formatSiderPairingTtl(params.remainingMs)}.`;
222
+ }
223
+
224
+ export function formatSiderPairingRetryMessage(params: {
225
+ pairingCode: string;
226
+ remainingMs: number;
227
+ retryAfterMs: number;
228
+ detail: string;
229
+ }): string {
230
+ return `Temporary connection issue while checking pairing status (${params.detail}). Retrying in ${formatSiderPairingTtl(params.retryAfterMs)}. Pairing code: ${formatHighlightedPairingCode(params.pairingCode)}. Pairing code expires in ${formatSiderPairingTtl(params.remainingMs)}.`;
175
231
  }
176
232
 
177
233
  export function createSiderPairingPendingUpdateReporter(params: {
234
+ pairingCode: string;
178
235
  report: (message: string) => void | Promise<void>;
179
236
  intervalMs?: number;
180
237
  }): (state: { remainingMs: number; pollIntervalMs: number }) => void | Promise<void> {
@@ -192,7 +249,12 @@ export function createSiderPairingPendingUpdateReporter(params: {
192
249
  }
193
250
  hasReported = true;
194
251
  lastReportedAt = now;
195
- await params.report(formatSiderPairingPendingMessage(remainingMs));
252
+ await params.report(
253
+ formatSiderPairingPendingMessage({
254
+ pairingCode: params.pairingCode,
255
+ remainingMs,
256
+ }),
257
+ );
196
258
  };
197
259
  }
198
260
 
@@ -262,19 +324,33 @@ export async function requestSiderPairing(): Promise<SiderPairingSession> {
262
324
  export async function readSiderPairingStatus(
263
325
  pairingToken: string,
264
326
  ): Promise<SiderPairStatusResponse | { status: "expired" }> {
265
- const response = await fetch(resolveSiderApiUrl(SIDER_PAIR_STATUS_API_PATH), {
266
- method: "GET",
267
- headers: {
268
- Authorization: formatAuthorizationHeader(pairingToken),
269
- "User-Agent": SIDER_USER_AGENT,
270
- },
271
- });
327
+ const url = resolveSiderApiUrl(SIDER_PAIR_STATUS_API_PATH);
328
+ let response: Response;
329
+ try {
330
+ response = await fetch(url, {
331
+ method: "GET",
332
+ headers: {
333
+ Authorization: formatAuthorizationHeader(pairingToken),
334
+ "User-Agent": SIDER_USER_AGENT,
335
+ },
336
+ });
337
+ } catch (error) {
338
+ if (isRetryableFetchError(error)) {
339
+ throw new SiderPairingTransientError(describeUnknownError(error));
340
+ }
341
+ throw error;
342
+ }
272
343
  if (response.status === 410) {
273
344
  return { status: "expired" };
274
345
  }
346
+ if ([502, 503, 504].includes(response.status)) {
347
+ throw new SiderPairingTransientError(
348
+ `server returned ${response.status} ${response.statusText}${await parseErrorDetail(response)}`,
349
+ );
350
+ }
275
351
  if (!response.ok) {
276
352
  throw new Error(
277
- `selfclaw pairing status failed (${response.status} ${response.statusText})${await parseErrorDetail(response)}`,
353
+ `selfclaw pairing status failed (${url} ${response.status} ${response.statusText})${await parseErrorDetail(response)}`,
278
354
  );
279
355
  }
280
356
  return parsePairStatusResponse(await readJsonResponse(response));
@@ -283,11 +359,31 @@ export async function readSiderPairingStatus(
283
359
  export async function waitForSiderPairing(params: {
284
360
  pairing: SiderPairingSession;
285
361
  onPending?: (state: { remainingMs: number; pollIntervalMs: number }) => void | Promise<void>;
362
+ onRetryableError?: (message: string) => void | Promise<void>;
286
363
  }): Promise<SiderPairingResult> {
287
364
  const expiresAtMs = params.pairing.expiresAt * 1000;
288
365
 
289
366
  while (true) {
290
- const status = await readSiderPairingStatus(params.pairing.pairingToken);
367
+ let status: SiderPairStatusResponse | { status: "expired" };
368
+ try {
369
+ status = await readSiderPairingStatus(params.pairing.pairingToken);
370
+ } catch (error) {
371
+ const remainingMs = Math.max(0, expiresAtMs - Date.now());
372
+ if (error instanceof SiderPairingTransientError && remainingMs > 0) {
373
+ const retryAfterMs = Math.min(params.pairing.pollIntervalMs, remainingMs);
374
+ await params.onRetryableError?.(
375
+ formatSiderPairingRetryMessage({
376
+ pairingCode: params.pairing.pairingCode,
377
+ remainingMs,
378
+ retryAfterMs,
379
+ detail: error.message,
380
+ }),
381
+ );
382
+ await sleep(retryAfterMs);
383
+ continue;
384
+ }
385
+ throw error;
386
+ }
291
387
  if (status.status === "paired") {
292
388
  return {
293
389
  clawId: status.claw_id,
@@ -323,7 +419,8 @@ export function formatSiderPairingInstructions(params: {
323
419
  "",
324
420
  "1. Install the Sider Chrome extension from the link above",
325
421
  "2. Click the Sider icon in your browser toolbar to open the side panel",
326
- '3. Click "Connect to OpenClaw" and enter the pairing code above',
422
+ "3. In the right sidebar, find and click the Claw icon (the paw-shaped icon, 2nd from top)",
423
+ `4. Enter the pairing code "${params.pairingCode}" and click Connect`,
327
424
  "",
328
425
  "I'm waiting for the connection...",
329
426
  ].join("\n");