@hywkp/test-openclaw-sider-cli 1.0.0
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 +78 -0
- package/README.zh_CN.md +80 -0
- package/cli.mjs +609 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# @sider-ai/chrome-openclaw-sider-cli
|
|
2
|
+
|
|
3
|
+
Official `npx` CLI for installing and connecting OpenClaw in Sider. It supports both Sider onboarding flows:
|
|
4
|
+
|
|
5
|
+
- Terminal pairing: install the plugin and immediately run `openclaw channels login --channel chrome-openclaw-sider`
|
|
6
|
+
- Setup token: write `channels.chrome-openclaw-sider.setupToken`, restart the gateway, and wait for the plugin to exchange it for a long-lived token
|
|
7
|
+
|
|
8
|
+
## Prerequisites
|
|
9
|
+
|
|
10
|
+
OpenClaw must already be installed and the `openclaw` command must be available:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
openclaw --version
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Minimum required version: `openclaw >= 2026.3.22`
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
Install the plugin and start the pairing flow:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx -y @sider-ai/chrome-openclaw-sider-cli install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Install with a one-time setup token and wait for registration to complete:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx -y @sider-ai/chrome-openclaw-sider-cli install --setup-token '<one-time-token>'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Write a long-lived relay token directly:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx -y @sider-ai/chrome-openclaw-sider-cli install --token '<token>'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Start pairing again for an already installed plugin:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx -y @sider-ai/chrome-openclaw-sider-cli connect
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Commands
|
|
45
|
+
|
|
46
|
+
### `install`
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx -y @sider-ai/chrome-openclaw-sider-cli install [options]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Common options:
|
|
53
|
+
|
|
54
|
+
- `--setup-token <token>`: write `channels.chrome-openclaw-sider.setupToken`, restart the gateway, and poll until the plugin writes back `channels.chrome-openclaw-sider.token`
|
|
55
|
+
- `--token <token>`: write `channels.chrome-openclaw-sider.token` directly
|
|
56
|
+
- `--relay-id <id>`: also write `channels.chrome-openclaw-sider.relayId`
|
|
57
|
+
- `--account <id>`: target a named account under `channels.chrome-openclaw-sider.accounts.<id>`
|
|
58
|
+
- `--timeout-ms <ms>`: maximum wait time in setup-token mode, default `60000`
|
|
59
|
+
- `--no-connect`: install or update the plugin only, without starting terminal pairing
|
|
60
|
+
- `--no-restart`: skip `openclaw gateway restart`
|
|
61
|
+
|
|
62
|
+
### `connect`
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx -y @sider-ai/chrome-openclaw-sider-cli connect [options]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Common options:
|
|
69
|
+
|
|
70
|
+
- `--account <id>`: start pairing for the specified account
|
|
71
|
+
- `--no-restart`: skip the gateway restart after pairing succeeds
|
|
72
|
+
|
|
73
|
+
## Behavior
|
|
74
|
+
|
|
75
|
+
- The plugin is installed by default via `openclaw plugins install chrome-openclaw-sider`
|
|
76
|
+
- If the plugin is already present, the CLI automatically tries `openclaw plugins update chrome-openclaw-sider`
|
|
77
|
+
- `--setup-token` and `--token` cannot be used together
|
|
78
|
+
- In `--setup-token` mode, the CLI checks that `setupToken` has been consumed and that the long-lived `token` has been written back to the config
|
package/README.zh_CN.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @sider-ai/chrome-openclaw-sider-cli
|
|
2
|
+
|
|
3
|
+
[English](./README.md)
|
|
4
|
+
|
|
5
|
+
官方 `npx` CLI,用来安装插件并在 Sider 中连接 OpenClaw,支持 `chrome-openclaw-sider` 的两种初始化方式:
|
|
6
|
+
|
|
7
|
+
- 终端配对:安装后直接执行 `openclaw channels login --channel chrome-openclaw-sider`
|
|
8
|
+
- setup token:写入 `channels.chrome-openclaw-sider.setupToken`,重启 gateway,并等待插件换取长期 token
|
|
9
|
+
|
|
10
|
+
## 前提
|
|
11
|
+
|
|
12
|
+
机器上需要已经安装 OpenClaw,并且 `openclaw` 命令可用:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
openclaw --version
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
最低要求:`openclaw >= 2026.3.22`
|
|
19
|
+
|
|
20
|
+
## 快速开始
|
|
21
|
+
|
|
22
|
+
安装插件并进入配对流程:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx -y @sider-ai/chrome-openclaw-sider-cli install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
用 one-time token 安装并完成注册:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx -y @sider-ai/chrome-openclaw-sider-cli install --setup-token '<one-time-token>'
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
直接写入长期 relay token:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx -y @sider-ai/chrome-openclaw-sider-cli install --token '<token>'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
对已安装插件重新发起配对:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx -y @sider-ai/chrome-openclaw-sider-cli connect
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 命令
|
|
47
|
+
|
|
48
|
+
### `install`
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx -y @sider-ai/chrome-openclaw-sider-cli install [options]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
常用选项:
|
|
55
|
+
|
|
56
|
+
- `--setup-token <token>`:写入 `channels.chrome-openclaw-sider.setupToken`,重启 gateway,并轮询等待插件把长期 `channels.chrome-openclaw-sider.token` 写回配置
|
|
57
|
+
- `--token <token>`:直接写入长期 `channels.chrome-openclaw-sider.token`
|
|
58
|
+
- `--relay-id <id>`:额外写入 `channels.chrome-openclaw-sider.relayId`
|
|
59
|
+
- `--account <id>`:写入命名账号 `channels.chrome-openclaw-sider.accounts.<id>`
|
|
60
|
+
- `--timeout-ms <ms>`:setup token 模式下的最长等待时间,默认 `60000`
|
|
61
|
+
- `--no-connect`:仅安装或更新插件,不自动进入终端配对
|
|
62
|
+
- `--no-restart`:跳过 `openclaw gateway restart`
|
|
63
|
+
|
|
64
|
+
### `connect`
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npx -y @sider-ai/chrome-openclaw-sider-cli connect [options]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
常用选项:
|
|
71
|
+
|
|
72
|
+
- `--account <id>`:对指定账号发起配对
|
|
73
|
+
- `--no-restart`:配对成功后跳过 gateway restart
|
|
74
|
+
|
|
75
|
+
## 行为说明
|
|
76
|
+
|
|
77
|
+
- 插件安装默认走 `openclaw plugins install chrome-openclaw-sider`
|
|
78
|
+
- 若本地已存在,则自动尝试 `openclaw plugins update chrome-openclaw-sider`
|
|
79
|
+
- `--setup-token` 和 `--token` 不能混用
|
|
80
|
+
- `--setup-token` 模式下,CLI 会检测 `setupToken` 是否已被插件消费,并确认长期 `token` 已经写回配置
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
const PLUGIN_SPEC = "@hywkp/test-openclaw-sider";
|
|
6
|
+
const CHANNEL_ID = "test-openclaw-sider";
|
|
7
|
+
const MIN_HOST_VERSION = "2026.3.22";
|
|
8
|
+
const DEFAULT_WAIT_TIMEOUT_MS = 60_000;
|
|
9
|
+
const WAIT_POLL_INTERVAL_MS = 2_000;
|
|
10
|
+
|
|
11
|
+
function log(msg) {
|
|
12
|
+
console.log(`\x1b[36m[sider]\x1b[0m ${msg}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function warn(msg) {
|
|
16
|
+
console.warn(`\x1b[33m[sider]\x1b[0m ${msg}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function error(msg) {
|
|
20
|
+
console.error(`\x1b[31m[sider]\x1b[0m ${msg}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sleep(ms) {
|
|
24
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function quoteShellArg(value) {
|
|
28
|
+
if (/^[A-Za-z0-9_./:@=-]+$/.test(value)) {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatCommand(parts) {
|
|
35
|
+
return parts.map((part) => quoteShellArg(part)).join(" ");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function trimMaybe(value) {
|
|
39
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toCombinedOutput(result) {
|
|
43
|
+
return `${result.stdout || ""}\n${result.stderr || ""}`.trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function runCommand(command, args, { input, inherit = false } = {}) {
|
|
47
|
+
const result = spawnSync(command, args, {
|
|
48
|
+
encoding: "utf8",
|
|
49
|
+
input,
|
|
50
|
+
shell: process.platform === "win32",
|
|
51
|
+
stdio: inherit ? "inherit" : ["pipe", "pipe", "pipe"],
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
command,
|
|
55
|
+
args,
|
|
56
|
+
status: typeof result.status === "number" ? result.status : 1,
|
|
57
|
+
stdout: inherit ? "" : result.stdout || "",
|
|
58
|
+
stderr: inherit ? "" : result.stderr || "",
|
|
59
|
+
error: result.error,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function runOpenclaw(args, options) {
|
|
64
|
+
return runCommand("openclaw", args, options);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ensureSuccess(result, context) {
|
|
68
|
+
if (result.error) {
|
|
69
|
+
throw result.error;
|
|
70
|
+
}
|
|
71
|
+
if (result.status !== 0) {
|
|
72
|
+
const commandText = formatCommand([result.command, ...result.args]);
|
|
73
|
+
const combined = toCombinedOutput(result);
|
|
74
|
+
const err = new Error(
|
|
75
|
+
`${context} failed with exit code ${result.status}: ${commandText}${combined ? `\n${combined}` : ""}`,
|
|
76
|
+
);
|
|
77
|
+
err.stdout = result.stdout;
|
|
78
|
+
err.stderr = result.stderr;
|
|
79
|
+
err.status = result.status;
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isOpenClawInstalled() {
|
|
86
|
+
const result = runOpenclaw(["--version"]);
|
|
87
|
+
return !result.error && result.status === 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getOpenclawVersion() {
|
|
91
|
+
const result = runOpenclaw(["--version"]);
|
|
92
|
+
if (result.error || result.status !== 0) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const match = `${result.stdout || ""} ${result.stderr || ""}`.match(/(\d+\.\d+\.\d+)/);
|
|
96
|
+
return match ? match[1] : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function compareVersions(a, b) {
|
|
100
|
+
const left = a.split(".").map((part) => Number.parseInt(part, 10));
|
|
101
|
+
const right = b.split(".").map((part) => Number.parseInt(part, 10));
|
|
102
|
+
const length = Math.max(left.length, right.length);
|
|
103
|
+
for (let i = 0; i < length; i += 1) {
|
|
104
|
+
const lhs = Number.isFinite(left[i]) ? left[i] : 0;
|
|
105
|
+
const rhs = Number.isFinite(right[i]) ? right[i] : 0;
|
|
106
|
+
if (lhs < rhs) return -1;
|
|
107
|
+
if (lhs > rhs) return 1;
|
|
108
|
+
}
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function assertOpenclawReady() {
|
|
113
|
+
if (!isOpenClawInstalled()) {
|
|
114
|
+
error("openclaw was not found. Please install it first:");
|
|
115
|
+
console.log(" npm install -g openclaw");
|
|
116
|
+
console.log(" See https://docs.openclaw.ai/install");
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const hostVersion = getOpenclawVersion();
|
|
121
|
+
if (!hostVersion) {
|
|
122
|
+
error("Unable to detect the openclaw version. Please make sure `openclaw --version` works.");
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
log(`Detected OpenClaw version: ${hostVersion}`);
|
|
127
|
+
if (compareVersions(hostVersion, MIN_HOST_VERSION) < 0) {
|
|
128
|
+
error(`Your OpenClaw version is too old. Requires at least ${MIN_HOST_VERSION}`);
|
|
129
|
+
console.log(
|
|
130
|
+
` Please upgrade OpenClaw, then retry ${formatCommand(["npx", "-y", "@sider-ai/chrome-openclaw-sider-cli", "install"])}`,
|
|
131
|
+
);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function printCapturedOutput(result) {
|
|
137
|
+
const stdout = trimMaybe(result.stdout);
|
|
138
|
+
const stderr = trimMaybe(result.stderr);
|
|
139
|
+
if (stdout) {
|
|
140
|
+
console.log(stdout);
|
|
141
|
+
}
|
|
142
|
+
if (stderr) {
|
|
143
|
+
console.error(stderr);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function looksLikeAlreadyInstalled(result) {
|
|
148
|
+
return /already exists/i.test(toCombinedOutput(result));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function installOrUpdatePlugin() {
|
|
152
|
+
log(`Installing plugin ${PLUGIN_SPEC}...`);
|
|
153
|
+
const installResult = runOpenclaw(["plugins", "install", PLUGIN_SPEC]);
|
|
154
|
+
if (!installResult.error && installResult.status === 0) {
|
|
155
|
+
printCapturedOutput(installResult);
|
|
156
|
+
log("Plugin installed.");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (looksLikeAlreadyInstalled(installResult)) {
|
|
161
|
+
log("Plugin already installed, trying to update...");
|
|
162
|
+
const updateResult = runOpenclaw(["plugins", "update", CHANNEL_ID], { input: "y\n" });
|
|
163
|
+
const updateOutput = toCombinedOutput(updateResult);
|
|
164
|
+
const updateFailed =
|
|
165
|
+
Boolean(updateResult.error) ||
|
|
166
|
+
updateResult.status !== 0 ||
|
|
167
|
+
/Failed to (update|check)|aborted:/i.test(updateOutput);
|
|
168
|
+
|
|
169
|
+
if (!updateFailed) {
|
|
170
|
+
printCapturedOutput(updateResult);
|
|
171
|
+
log("Plugin updated.");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (updateOutput) {
|
|
176
|
+
console.error(updateOutput);
|
|
177
|
+
}
|
|
178
|
+
warn("Plugin update failed. Continuing with the currently installed version.");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (toCombinedOutput(installResult)) {
|
|
183
|
+
console.error(toCombinedOutput(installResult));
|
|
184
|
+
}
|
|
185
|
+
error("Plugin installation failed. Please run this manually:");
|
|
186
|
+
console.log(` ${formatCommand(["openclaw", "plugins", "install", PLUGIN_SPEC])}`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function normalizeAccountId(raw) {
|
|
191
|
+
const trimmed = trimMaybe(raw);
|
|
192
|
+
return trimmed || "default";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function accountConfigBase(accountId) {
|
|
196
|
+
const normalized = normalizeAccountId(accountId);
|
|
197
|
+
if (normalized === "default") {
|
|
198
|
+
return `channels.${CHANNEL_ID}`;
|
|
199
|
+
}
|
|
200
|
+
return `channels.${CHANNEL_ID}.accounts.${normalized}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function accountConfigPath(accountId, key) {
|
|
204
|
+
return `${accountConfigBase(accountId)}.${key}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function configSet(path, value) {
|
|
208
|
+
ensureSuccess(
|
|
209
|
+
runOpenclaw(["config", "set", path, JSON.stringify(value), "--strict-json"]),
|
|
210
|
+
`Config set (${path})`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isMissingConfigPath(result, path) {
|
|
215
|
+
return toCombinedOutput(result).includes(`Config path not found: ${path}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function configUnset(path) {
|
|
219
|
+
const result = runOpenclaw(["config", "unset", path]);
|
|
220
|
+
if (!result.error && result.status === 0) {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
if (isMissingConfigPath(result, path)) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
ensureSuccess(result, `Config unset (${path})`);
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function configPathExists(path) {
|
|
231
|
+
const result = runOpenclaw(["config", "get", path, "--json"]);
|
|
232
|
+
if (!result.error && result.status === 0) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
if (isMissingConfigPath(result, path)) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
ensureSuccess(result, `Config get (${path})`);
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function applyCommonConfig({ accountId, relayId }) {
|
|
243
|
+
configSet(accountConfigPath(accountId, "enabled"), true);
|
|
244
|
+
if (trimMaybe(relayId)) {
|
|
245
|
+
configSet(accountConfigPath(accountId, "relayId"), relayId.trim());
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function configureSetupToken({ accountId, token, relayId }) {
|
|
250
|
+
const tokenPath = accountConfigPath(accountId, "token");
|
|
251
|
+
const setupTokenPath = accountConfigPath(accountId, "setupToken");
|
|
252
|
+
|
|
253
|
+
log("Writing setup token configuration...");
|
|
254
|
+
applyCommonConfig({ accountId, relayId });
|
|
255
|
+
configUnset(tokenPath);
|
|
256
|
+
configSet(setupTokenPath, token);
|
|
257
|
+
log(`Wrote channels.${CHANNEL_ID}.setupToken.`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function configureRelayToken({ accountId, token, relayId }) {
|
|
261
|
+
const tokenPath = accountConfigPath(accountId, "token");
|
|
262
|
+
const setupTokenPath = accountConfigPath(accountId, "setupToken");
|
|
263
|
+
|
|
264
|
+
log("Writing long-lived relay token configuration...");
|
|
265
|
+
applyCommonConfig({ accountId, relayId });
|
|
266
|
+
configUnset(setupTokenPath);
|
|
267
|
+
configSet(tokenPath, token);
|
|
268
|
+
log(`Wrote channels.${CHANNEL_ID}.token.`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function restartGateway() {
|
|
272
|
+
log("Restarting OpenClaw Gateway...");
|
|
273
|
+
const result = runOpenclaw(["gateway", "restart"], { inherit: true });
|
|
274
|
+
if (!result.error && result.status === 0) {
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
error("Restart failed. Please run this manually:");
|
|
278
|
+
console.log(` ${formatCommand(["openclaw", "gateway", "restart"])}`);
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function waitForSetupCompletion({ accountId, timeoutMs }) {
|
|
283
|
+
const tokenPath = accountConfigPath(accountId, "token");
|
|
284
|
+
const setupTokenPath = accountConfigPath(accountId, "setupToken");
|
|
285
|
+
const deadline = Date.now() + timeoutMs;
|
|
286
|
+
|
|
287
|
+
log("Waiting for Sider to finish registration and write back the long-lived token...");
|
|
288
|
+
while (Date.now() <= deadline) {
|
|
289
|
+
const hasToken = configPathExists(tokenPath);
|
|
290
|
+
const hasSetupToken = configPathExists(setupTokenPath);
|
|
291
|
+
if (hasToken && !hasSetupToken) {
|
|
292
|
+
log("Sider registration succeeded. The long-lived token has been written back to the config.");
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
await sleep(WAIT_POLL_INTERVAL_MS);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function runPairingLogin({ accountId }) {
|
|
302
|
+
const command = buildLoginCommand(accountId);
|
|
303
|
+
const args = command.slice(1);
|
|
304
|
+
|
|
305
|
+
log("Plugin is ready. Starting Sider pairing...");
|
|
306
|
+
const result = runOpenclaw(args, { inherit: true });
|
|
307
|
+
if (!result.error && result.status === 0) {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
console.log();
|
|
312
|
+
error("Pairing did not complete. You can retry later with:");
|
|
313
|
+
console.log(` ${formatCommand(command)}`);
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function buildLoginCommand(accountId) {
|
|
318
|
+
const command = ["openclaw", "channels", "login", "--channel", CHANNEL_ID];
|
|
319
|
+
const normalized = normalizeAccountId(accountId);
|
|
320
|
+
if (normalized !== "default") {
|
|
321
|
+
command.push("--account", normalized);
|
|
322
|
+
}
|
|
323
|
+
return command;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function readRequiredValue(argv, index, flag) {
|
|
327
|
+
const value = argv[index + 1];
|
|
328
|
+
const trimmed = trimMaybe(value);
|
|
329
|
+
if (!trimmed) {
|
|
330
|
+
throw new Error(`Missing value for ${flag}`);
|
|
331
|
+
}
|
|
332
|
+
return { value: trimmed, nextIndex: index + 1 };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function readPositiveInteger(raw, flag) {
|
|
336
|
+
const value = Number.parseInt(raw, 10);
|
|
337
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
338
|
+
throw new Error(`${flag} must be a positive integer`);
|
|
339
|
+
}
|
|
340
|
+
return value;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function parseInstallArgs(argv) {
|
|
344
|
+
const opts = {
|
|
345
|
+
accountId: "default",
|
|
346
|
+
connect: true,
|
|
347
|
+
relayId: undefined,
|
|
348
|
+
relayToken: undefined,
|
|
349
|
+
restart: true,
|
|
350
|
+
setupToken: undefined,
|
|
351
|
+
timeoutMs: DEFAULT_WAIT_TIMEOUT_MS,
|
|
352
|
+
help: false,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
356
|
+
const arg = argv[i];
|
|
357
|
+
switch (arg) {
|
|
358
|
+
case "--token": {
|
|
359
|
+
const next = readRequiredValue(argv, i, arg);
|
|
360
|
+
opts.relayToken = next.value;
|
|
361
|
+
i = next.nextIndex;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
case "--setup-token":
|
|
365
|
+
case "--one-time-token": {
|
|
366
|
+
const next = readRequiredValue(argv, i, arg);
|
|
367
|
+
opts.setupToken = next.value;
|
|
368
|
+
i = next.nextIndex;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
case "--relay-token":
|
|
372
|
+
case "--direct-token": {
|
|
373
|
+
const next = readRequiredValue(argv, i, arg);
|
|
374
|
+
opts.relayToken = next.value;
|
|
375
|
+
i = next.nextIndex;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
case "--relay-id": {
|
|
379
|
+
const next = readRequiredValue(argv, i, arg);
|
|
380
|
+
opts.relayId = next.value;
|
|
381
|
+
i = next.nextIndex;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
case "--account": {
|
|
385
|
+
const next = readRequiredValue(argv, i, arg);
|
|
386
|
+
opts.accountId = normalizeAccountId(next.value);
|
|
387
|
+
i = next.nextIndex;
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
case "--timeout-ms":
|
|
391
|
+
case "--wait-timeout-ms": {
|
|
392
|
+
const next = readRequiredValue(argv, i, arg);
|
|
393
|
+
opts.timeoutMs = readPositiveInteger(next.value, arg);
|
|
394
|
+
i = next.nextIndex;
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
case "--no-connect":
|
|
398
|
+
opts.connect = false;
|
|
399
|
+
break;
|
|
400
|
+
case "--no-restart":
|
|
401
|
+
opts.restart = false;
|
|
402
|
+
break;
|
|
403
|
+
case "--help":
|
|
404
|
+
case "-h":
|
|
405
|
+
opts.help = true;
|
|
406
|
+
break;
|
|
407
|
+
default:
|
|
408
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (opts.setupToken && opts.relayToken) {
|
|
413
|
+
throw new Error("Do not mix --setup-token with --token.");
|
|
414
|
+
}
|
|
415
|
+
return opts;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function parseConnectArgs(argv) {
|
|
419
|
+
const opts = {
|
|
420
|
+
accountId: "default",
|
|
421
|
+
restart: true,
|
|
422
|
+
help: false,
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
426
|
+
const arg = argv[i];
|
|
427
|
+
switch (arg) {
|
|
428
|
+
case "--account": {
|
|
429
|
+
const next = readRequiredValue(argv, i, arg);
|
|
430
|
+
opts.accountId = normalizeAccountId(next.value);
|
|
431
|
+
i = next.nextIndex;
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
case "--no-restart":
|
|
435
|
+
opts.restart = false;
|
|
436
|
+
break;
|
|
437
|
+
case "--help":
|
|
438
|
+
case "-h":
|
|
439
|
+
opts.help = true;
|
|
440
|
+
break;
|
|
441
|
+
default:
|
|
442
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return opts;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function installCommand(argv) {
|
|
450
|
+
const opts = parseInstallArgs(argv);
|
|
451
|
+
if (opts.help) {
|
|
452
|
+
printInstallHelp();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
assertOpenclawReady();
|
|
457
|
+
installOrUpdatePlugin();
|
|
458
|
+
|
|
459
|
+
if (opts.setupToken) {
|
|
460
|
+
configureSetupToken({
|
|
461
|
+
accountId: opts.accountId,
|
|
462
|
+
token: opts.setupToken,
|
|
463
|
+
relayId: opts.relayId,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
if (!opts.restart) {
|
|
467
|
+
warn("Skipped gateway restart. Please run the following command manually to complete registration:");
|
|
468
|
+
console.log(` ${formatCommand(["openclaw", "gateway", "restart"])}`);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!restartGateway()) {
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const completed = await waitForSetupCompletion({
|
|
477
|
+
accountId: opts.accountId,
|
|
478
|
+
timeoutMs: opts.timeoutMs,
|
|
479
|
+
});
|
|
480
|
+
if (!completed) {
|
|
481
|
+
error("Timed out waiting for registration. Please check your network or verify manually later:");
|
|
482
|
+
console.log(` ${formatCommand(["openclaw", "channels", "status"])}`);
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (opts.relayToken) {
|
|
489
|
+
configureRelayToken({
|
|
490
|
+
accountId: opts.accountId,
|
|
491
|
+
token: opts.relayToken,
|
|
492
|
+
relayId: opts.relayId,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
if (!opts.restart) {
|
|
496
|
+
warn("Skipped gateway restart. Please run the following command manually to apply the configuration:");
|
|
497
|
+
console.log(` ${formatCommand(["openclaw", "gateway", "restart"])}`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
restartGateway();
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (!opts.connect) {
|
|
506
|
+
log("Plugin is installed. Connection flow was not started.");
|
|
507
|
+
console.log(` ${formatCommand(buildLoginCommand(opts.accountId))}`);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const paired = runPairingLogin({ accountId: opts.accountId });
|
|
512
|
+
if (paired && opts.restart) {
|
|
513
|
+
restartGateway();
|
|
514
|
+
} else if (paired && !opts.restart) {
|
|
515
|
+
warn("Skipped gateway restart. Please run the following command manually to activate the new connection:");
|
|
516
|
+
console.log(` ${formatCommand(["openclaw", "gateway", "restart"])}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function connectCommand(argv) {
|
|
521
|
+
const opts = parseConnectArgs(argv);
|
|
522
|
+
if (opts.help) {
|
|
523
|
+
printConnectHelp();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
assertOpenclawReady();
|
|
528
|
+
const paired = runPairingLogin({ accountId: opts.accountId });
|
|
529
|
+
if (paired && opts.restart) {
|
|
530
|
+
restartGateway();
|
|
531
|
+
} else if (paired && !opts.restart) {
|
|
532
|
+
warn("Skipped gateway restart. Please run the following command manually to activate the new connection:");
|
|
533
|
+
console.log(` ${formatCommand(["openclaw", "gateway", "restart"])}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function printHelp() {
|
|
538
|
+
console.log(`
|
|
539
|
+
Usage: npx -y @sider-ai/chrome-openclaw-sider-cli <command>
|
|
540
|
+
|
|
541
|
+
Commands:
|
|
542
|
+
install Install or update the Sider plugin; supports setup token, relay token, or terminal pairing
|
|
543
|
+
connect Start pairing again for an installed Sider plugin
|
|
544
|
+
help Show help
|
|
545
|
+
|
|
546
|
+
Examples:
|
|
547
|
+
npx -y @sider-ai/chrome-openclaw-sider-cli install
|
|
548
|
+
npx -y @sider-ai/chrome-openclaw-sider-cli install --setup-token '<one-time-token>'
|
|
549
|
+
npx -y @sider-ai/chrome-openclaw-sider-cli install --token '<token>'
|
|
550
|
+
npx -y @sider-ai/chrome-openclaw-sider-cli connect
|
|
551
|
+
`);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function printInstallHelp() {
|
|
555
|
+
console.log(`
|
|
556
|
+
Usage: npx -y @sider-ai/chrome-openclaw-sider-cli install [options]
|
|
557
|
+
|
|
558
|
+
Options:
|
|
559
|
+
--setup-token <token> Write channels.chrome-openclaw-sider.setupToken and wait for registration to complete
|
|
560
|
+
--token <token> Write channels.chrome-openclaw-sider.token directly
|
|
561
|
+
--relay-id <id> Optional relayId; defaults to existing config or the plugin default
|
|
562
|
+
--account <id> Account id, defaults to default
|
|
563
|
+
--timeout-ms <ms> Wait timeout for setup-token mode, defaults to ${DEFAULT_WAIT_TIMEOUT_MS}
|
|
564
|
+
--no-connect Install the plugin only; do not start terminal pairing
|
|
565
|
+
--no-restart Skip gateway restart
|
|
566
|
+
-h, --help Show help
|
|
567
|
+
`);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function printConnectHelp() {
|
|
571
|
+
console.log(`
|
|
572
|
+
Usage: npx -y @sider-ai/chrome-openclaw-sider-cli connect [options]
|
|
573
|
+
|
|
574
|
+
Options:
|
|
575
|
+
--account <id> Account id, defaults to default
|
|
576
|
+
--no-restart Skip gateway restart
|
|
577
|
+
-h, --help Show help
|
|
578
|
+
`);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async function main() {
|
|
582
|
+
const command = process.argv[2];
|
|
583
|
+
const argv = process.argv.slice(3);
|
|
584
|
+
|
|
585
|
+
switch (command) {
|
|
586
|
+
case "install":
|
|
587
|
+
await installCommand(argv);
|
|
588
|
+
break;
|
|
589
|
+
case "connect":
|
|
590
|
+
await connectCommand(argv);
|
|
591
|
+
break;
|
|
592
|
+
case "help":
|
|
593
|
+
case "--help":
|
|
594
|
+
case "-h":
|
|
595
|
+
printHelp();
|
|
596
|
+
break;
|
|
597
|
+
default:
|
|
598
|
+
if (command) {
|
|
599
|
+
error(`Unknown command: ${command}`);
|
|
600
|
+
}
|
|
601
|
+
printHelp();
|
|
602
|
+
process.exit(command ? 1 : 0);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
await main().catch((err) => {
|
|
607
|
+
error(err instanceof Error ? err.message : String(err));
|
|
608
|
+
process.exit(1);
|
|
609
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hywkp/test-openclaw-sider-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official CLI for installing and connecting the chrome-openclaw-sider plugin",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"chrome-openclaw-sider-cli": "./cli.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"cli.mjs",
|
|
12
|
+
"README.md",
|
|
13
|
+
"README.zh_CN.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"openclaw": ">=2026.3.22"
|
|
18
|
+
},
|
|
19
|
+
"peerDependenciesMeta": {
|
|
20
|
+
"openclaw": {
|
|
21
|
+
"optional": true
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"openclaw": "2026.3.22"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22"
|
|
29
|
+
}
|
|
30
|
+
}
|