chrome-openclaw-sider 1.0.1 → 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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "chrome-openclaw-sider",
3
3
  "private": false,
4
- "version": "1.0.1",
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");