@hywkp/test-openclaw-sider 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.
@@ -0,0 +1,64 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import {
3
+ SIDER_CHANNEL_ID,
4
+ SIDER_REMOTE_BROWSER_ENABLE_ENV,
5
+ readSiderRemoteBrowserEnableEnv,
6
+ } from "./config.js";
7
+
8
+ export { SIDER_REMOTE_BROWSER_ENABLE_ENV } from "./config.js";
9
+
10
+ const SIDER_REMOTE_BROWSER_PREPEND_CONTEXT = [
11
+ "Sider remote-browser mode is enabled.",
12
+ `When the active channel is \`${SIDER_CHANNEL_ID}\` and the user requests browser work, you must use the bundled \`remote-browser\` skill and the \`mcporter remote-browser\` MCP server.`,
13
+ "Do not fall back to built-in browser tools. If `remote-browser` is unavailable, stop and tell the user how to repair the local MCP setup.",
14
+ ].join("\n");
15
+
16
+ const SIDER_REMOTE_BROWSER_PREPEND_SYSTEM_CONTEXT = [
17
+ `Browser operations for \`channel=${SIDER_CHANNEL_ID}\` must go through \`remote-browser\`.`,
18
+ "Use the user's real browser via MCPorter, not the built-in browser tool.",
19
+ ].join("\n");
20
+
21
+ const SIDER_REMOTE_BROWSER_APPEND_SYSTEM_CONTEXT =
22
+ "If the request is not a browser task, continue normally. If it is a browser task and `remote-browser` is unavailable, report that clearly instead of improvising a fallback.";
23
+
24
+ type RemoteBrowserPromptInjection = {
25
+ prependContext: string;
26
+ prependSystemContext: string;
27
+ appendSystemContext: string;
28
+ };
29
+
30
+ function isTruthyEnv(value: string | undefined): boolean {
31
+ if (!value) {
32
+ return false;
33
+ }
34
+ return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
35
+ }
36
+
37
+ function isRemoteBrowserSupportEnabled(): boolean {
38
+ return isTruthyEnv(readSiderRemoteBrowserEnableEnv());
39
+ }
40
+
41
+ function buildRemoteBrowserPromptInjection(): RemoteBrowserPromptInjection {
42
+ return {
43
+ prependContext: SIDER_REMOTE_BROWSER_PREPEND_CONTEXT,
44
+ prependSystemContext: SIDER_REMOTE_BROWSER_PREPEND_SYSTEM_CONTEXT,
45
+ appendSystemContext: SIDER_REMOTE_BROWSER_APPEND_SYSTEM_CONTEXT,
46
+ };
47
+ }
48
+
49
+ export function registerSiderRemoteBrowserSupport(api: OpenClawPluginApi): void {
50
+ if (!isRemoteBrowserSupportEnabled()) {
51
+ return;
52
+ }
53
+
54
+ api.on("before_prompt_build", async (_event, ctx) => {
55
+ if (ctx.channelId !== SIDER_CHANNEL_ID) {
56
+ return;
57
+ }
58
+ // Preserve newer prompt-injection fields for hosts that support them while
59
+ // remaining compatible with the older SDK type surface.
60
+ return buildRemoteBrowserPromptInjection() as unknown as {
61
+ prependContext: string;
62
+ };
63
+ });
64
+ }
@@ -0,0 +1,431 @@
1
+ import {
2
+ DEFAULT_ACCOUNT_ID,
3
+ normalizeAccountId,
4
+ type ChannelSetupAdapter,
5
+ type ChannelSetupInput,
6
+ type OpenClawConfig,
7
+ } from "openclaw/plugin-sdk/setup";
8
+ import {
9
+ cloneOpenClawConfig,
10
+ formatAuthorizationHeader,
11
+ resolveSiderApiUrl,
12
+ resolveWritableSiderAccountConfig,
13
+ } from "./auth.js";
14
+ import { SIDER_CHANNEL_ID } from "./config.js";
15
+ import { SIDER_USER_AGENT } from "./user-agent.js";
16
+
17
+ export const SIDER_PAIR_REQUEST_API_PATH = "/v1/claws/pair/request";
18
+ export const SIDER_PAIR_STATUS_API_PATH = "/v1/claws/pair/status";
19
+ export const SIDER_EXTENSION_INSTALL_URL = "https://chromewebstore.google.com/detail/sider-chat-with-all-ai-gp/difoiogjjojoaoomphldepapgpbgkhkb?utm_source=openclaw";
20
+ const DEFAULT_POLL_INTERVAL_MS = 5_000;
21
+ const DEFAULT_PENDING_UPDATE_INTERVAL_MS = 30_000;
22
+ const ANSI_ESCAPE = "\u001B";
23
+ const ANSI_RESET = `${ANSI_ESCAPE}[0m`;
24
+ const ANSI_BOLD = `${ANSI_ESCAPE}[1m`;
25
+ const ANSI_CODE_COLOR = `${ANSI_ESCAPE}[38;5;214m`;
26
+
27
+ type SiderPairRequestResponse = {
28
+ pairing_code: string;
29
+ pairing_token: string;
30
+ expires_at: number;
31
+ poll_interval?: number;
32
+ };
33
+
34
+ type SiderPairStatusResponse =
35
+ | {
36
+ status: "pending";
37
+ }
38
+ | {
39
+ status: "paired";
40
+ claw_id: string;
41
+ token: string;
42
+ };
43
+
44
+ export type SiderPairingSession = {
45
+ pairingCode: string;
46
+ pairingToken: string;
47
+ expiresAt: number;
48
+ pollIntervalMs: number;
49
+ };
50
+
51
+ export type SiderPairingResult = {
52
+ clawId: string;
53
+ token: string;
54
+ };
55
+
56
+ export class SiderPairingExpiredError extends Error {
57
+ constructor(message = "Sider pairing code expired") {
58
+ super(message);
59
+ this.name = "SiderPairingExpiredError";
60
+ }
61
+ }
62
+
63
+ export class SiderPairingTransientError extends Error {
64
+ constructor(message: string) {
65
+ super(message);
66
+ this.name = "SiderPairingTransientError";
67
+ }
68
+ }
69
+
70
+ function trimMaybe(value: unknown): string | undefined {
71
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
72
+ }
73
+
74
+ function readJsonObject(payload: unknown, errorMessage: string): Record<string, unknown> {
75
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
76
+ throw new Error(errorMessage);
77
+ }
78
+ return payload as Record<string, unknown>;
79
+ }
80
+
81
+ function parsePairRequestResponse(payload: unknown): SiderPairRequestResponse {
82
+ const data = readJsonObject(payload, "selfclaw pairing response is not a JSON object");
83
+ const pairingCode = trimMaybe(data.pairing_code);
84
+ const pairingToken = trimMaybe(data.pairing_token);
85
+ const expiresAt =
86
+ typeof data.expires_at === "number" && Number.isFinite(data.expires_at) ? data.expires_at : 0;
87
+ const pollInterval =
88
+ typeof data.poll_interval === "number" && Number.isFinite(data.poll_interval)
89
+ ? Math.floor(data.poll_interval)
90
+ : undefined;
91
+ if (!pairingCode || !pairingToken || expiresAt <= 0) {
92
+ throw new Error("selfclaw pairing response missing pairing_code/pairing_token/expires_at");
93
+ }
94
+ return {
95
+ pairing_code: pairingCode,
96
+ pairing_token: pairingToken,
97
+ expires_at: expiresAt,
98
+ poll_interval: pollInterval,
99
+ };
100
+ }
101
+
102
+ function parsePairStatusResponse(payload: unknown): SiderPairStatusResponse {
103
+ const data = readJsonObject(payload, "selfclaw pairing status is not a JSON object");
104
+ const status = trimMaybe(data.status);
105
+ if (status === "pending") {
106
+ return { status: "pending" };
107
+ }
108
+ if (status === "paired") {
109
+ const clawId = trimMaybe(data.claw_id);
110
+ const token = trimMaybe(data.token);
111
+ if (!clawId || !token) {
112
+ throw new Error("selfclaw pairing status missing claw_id/token");
113
+ }
114
+ return {
115
+ status: "paired",
116
+ claw_id: clawId,
117
+ token,
118
+ };
119
+ }
120
+ throw new Error("selfclaw pairing status missing valid status");
121
+ }
122
+
123
+ async function parseErrorDetail(response: Response): Promise<string> {
124
+ try {
125
+ const text = (await response.text()).trim();
126
+ return text ? `: ${text}` : "";
127
+ } catch {
128
+ return "";
129
+ }
130
+ }
131
+
132
+ async function readJsonResponse(response: Response): Promise<unknown> {
133
+ try {
134
+ return await response.json();
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+
140
+ function sleep(ms: number): Promise<void> {
141
+ return new Promise((resolve) => setTimeout(resolve, ms));
142
+ }
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
+
181
+ function supportsTerminalFormatting(): boolean {
182
+ return Boolean(process.stdout.isTTY);
183
+ }
184
+
185
+ function stripAnsiEscapes(value: string): string {
186
+ return value.replaceAll(ANSI_ESCAPE, "");
187
+ }
188
+
189
+ function formatTerminalLink(url: string, label = url): string {
190
+ const safeUrl = stripAnsiEscapes(url);
191
+ const safeLabel = stripAnsiEscapes(label);
192
+ if (!supportsTerminalFormatting()) {
193
+ return safeUrl;
194
+ }
195
+ return `\u001b]8;;${safeUrl}\u0007${safeLabel}\u001b]8;;\u0007`;
196
+ }
197
+
198
+ function formatHighlightedPairingCode(pairingCode: string): string {
199
+ const safePairingCode = stripAnsiEscapes(pairingCode);
200
+ if (!supportsTerminalFormatting()) {
201
+ return safePairingCode;
202
+ }
203
+ return `${ANSI_BOLD}${ANSI_CODE_COLOR}${safePairingCode}${ANSI_RESET}`;
204
+ }
205
+
206
+ function formatSiderExtensionInstallLink(): string {
207
+ return formatTerminalLink(SIDER_EXTENSION_INSTALL_URL);
208
+ }
209
+
210
+ export function formatSiderPairingTtl(remainingMs: number): string {
211
+ const totalSeconds = Math.max(0, Math.ceil(remainingMs / 1000));
212
+ const minutes = Math.floor(totalSeconds / 60);
213
+ const seconds = totalSeconds % 60;
214
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
215
+ }
216
+
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)}.`;
231
+ }
232
+
233
+ export function createSiderPairingPendingUpdateReporter(params: {
234
+ pairingCode: string;
235
+ report: (message: string) => void | Promise<void>;
236
+ intervalMs?: number;
237
+ }): (state: { remainingMs: number; pollIntervalMs: number }) => void | Promise<void> {
238
+ const intervalMs = Math.max(1_000, Math.floor(params.intervalMs ?? DEFAULT_PENDING_UPDATE_INTERVAL_MS));
239
+ let lastReportedAt = Date.now();
240
+ let hasReported = false;
241
+
242
+ return async ({ remainingMs }) => {
243
+ const now = Date.now();
244
+ const shouldReport = hasReported
245
+ ? now - lastReportedAt >= intervalMs
246
+ : now - lastReportedAt >= intervalMs || remainingMs <= intervalMs;
247
+ if (!shouldReport) {
248
+ return;
249
+ }
250
+ hasReported = true;
251
+ lastReportedAt = now;
252
+ await params.report(
253
+ formatSiderPairingPendingMessage({
254
+ pairingCode: params.pairingCode,
255
+ remainingMs,
256
+ }),
257
+ );
258
+ };
259
+ }
260
+
261
+ export function applySiderSetupAccountConfig(params: {
262
+ cfg: OpenClawConfig;
263
+ accountId: string;
264
+ input: Pick<ChannelSetupInput, "name" | "token">;
265
+ }): OpenClawConfig {
266
+ const next = cloneOpenClawConfig(params.cfg);
267
+ const accountCfg = resolveWritableSiderAccountConfig(next, params.accountId);
268
+ const token = trimMaybe(params.input.token);
269
+ const name = trimMaybe(params.input.name);
270
+
271
+ if (name) {
272
+ accountCfg.name = name;
273
+ }
274
+ if (token) {
275
+ accountCfg.token = token;
276
+ }
277
+ delete accountCfg.setupToken;
278
+ accountCfg.enabled = true;
279
+
280
+ return next;
281
+ }
282
+
283
+ export const siderSetupAdapter: ChannelSetupAdapter = {
284
+ resolveAccountId: ({ accountId }) => normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID,
285
+ validateInput: ({ input }) => {
286
+ if (input.useEnv) {
287
+ return "Sider setup does not support --use-env.";
288
+ }
289
+ return null;
290
+ },
291
+ applyAccountConfig: ({ cfg, accountId, input }) =>
292
+ applySiderSetupAccountConfig({
293
+ cfg,
294
+ accountId,
295
+ input,
296
+ }),
297
+ };
298
+
299
+ export async function requestSiderPairing(): Promise<SiderPairingSession> {
300
+ const url = resolveSiderApiUrl(SIDER_PAIR_REQUEST_API_PATH)
301
+ const response = await fetch(url, {
302
+ method: "POST",
303
+ headers: {
304
+ "User-Agent": SIDER_USER_AGENT,
305
+ },
306
+ });
307
+ if (!response.ok) {
308
+ throw new Error(
309
+ `selfclaw pairing request failed (${url} ${response.status} ${response.statusText})${await parseErrorDetail(response)}`,
310
+ );
311
+ }
312
+ const parsed = parsePairRequestResponse(await readJsonResponse(response));
313
+ return {
314
+ pairingCode: parsed.pairing_code,
315
+ pairingToken: parsed.pairing_token,
316
+ expiresAt: parsed.expires_at,
317
+ pollIntervalMs:
318
+ (parsed.poll_interval && parsed.poll_interval > 0
319
+ ? parsed.poll_interval * 1000
320
+ : DEFAULT_POLL_INTERVAL_MS),
321
+ };
322
+ }
323
+
324
+ export async function readSiderPairingStatus(
325
+ pairingToken: string,
326
+ ): Promise<SiderPairStatusResponse | { status: "expired" }> {
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
+ }
343
+ if (response.status === 410) {
344
+ return { status: "expired" };
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
+ }
351
+ if (!response.ok) {
352
+ throw new Error(
353
+ `selfclaw pairing status failed (${url} ${response.status} ${response.statusText})${await parseErrorDetail(response)}`,
354
+ );
355
+ }
356
+ return parsePairStatusResponse(await readJsonResponse(response));
357
+ }
358
+
359
+ export async function waitForSiderPairing(params: {
360
+ pairing: SiderPairingSession;
361
+ onPending?: (state: { remainingMs: number; pollIntervalMs: number }) => void | Promise<void>;
362
+ onRetryableError?: (message: string) => void | Promise<void>;
363
+ }): Promise<SiderPairingResult> {
364
+ const expiresAtMs = params.pairing.expiresAt * 1000;
365
+
366
+ while (true) {
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
+ }
387
+ if (status.status === "paired") {
388
+ return {
389
+ clawId: status.claw_id,
390
+ token: status.token,
391
+ };
392
+ }
393
+
394
+ const remainingMs = Math.max(0, expiresAtMs - Date.now());
395
+ if (status.status === "expired" || remainingMs <= 0) {
396
+ throw new SiderPairingExpiredError();
397
+ }
398
+
399
+ await params.onPending?.({
400
+ remainingMs,
401
+ pollIntervalMs: params.pairing.pollIntervalMs,
402
+ });
403
+ await sleep(Math.min(params.pairing.pollIntervalMs, remainingMs));
404
+ }
405
+ }
406
+
407
+ export function formatSiderPairingInstructions(params: {
408
+ pairingCode: string;
409
+ leadLine?: string;
410
+ }): string {
411
+ return [
412
+ params.leadLine?.trim() || "Plugin installed. One more step to connect:",
413
+ "",
414
+ "PAIRING CODE",
415
+ ` ${formatHighlightedPairingCode(params.pairingCode)}`,
416
+ "",
417
+ "CHROME EXTENSION",
418
+ ` ${formatSiderExtensionInstallLink()}`,
419
+ "",
420
+ "1. Install the Sider Chrome extension from the link above",
421
+ "2. Click the Sider icon in your browser toolbar to open the side panel",
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`,
424
+ "",
425
+ "I'm waiting for the connection...",
426
+ ].join("\n");
427
+ }
428
+
429
+ export function getSiderSetupChannelId(): string {
430
+ return SIDER_CHANNEL_ID;
431
+ }
@@ -0,0 +1,17 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ function readPluginVersion(): string {
6
+ try {
7
+ const dir = path.dirname(fileURLToPath(import.meta.url));
8
+ const pkgPath = path.resolve(dir, "..", "package.json");
9
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as { version?: string };
10
+ return pkg.version ?? "unknown";
11
+ } catch {
12
+ return "unknown";
13
+ }
14
+ }
15
+
16
+ export const SIDER_PLUGIN_VERSION = readPluginVersion();
17
+ export const SIDER_USER_AGENT = `openclaw-sider/${SIDER_PLUGIN_VERSION}`;