mateclaw-openclaw-plugin 0.1.2 → 0.1.4
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/LICENSE +21 -21
- package/README.md +128 -72
- package/package.json +38 -37
- package/src/cli.mjs +631 -16
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 MateClaw
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MateClaw
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,100 +1,156 @@
|
|
|
1
|
-
# mateclaw-openclaw-plugin
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/mateclaw-openclaw-plugin)
|
|
4
|
-
[](https://www.npmjs.com/package/mateclaw-openclaw-plugin)
|
|
5
|
-
[](./LICENSE)
|
|
6
|
-
|
|
7
|
-
This package provides a
|
|
8
|
-
|
|
1
|
+
# mateclaw-openclaw-plugin
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/mateclaw-openclaw-plugin)
|
|
4
|
+
[](https://www.npmjs.com/package/mateclaw-openclaw-plugin)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
|
|
7
|
+
This package provides a one-command local setup for MateClaw app-only flow:
|
|
8
|
+
|
|
9
9
|
- `doctor` checks OpenClaw, gateway, token, identity, LAN, and connector port
|
|
10
|
-
- `install` runs `doctor + auto-fix + optional gateway restart + connector start`
|
|
11
|
-
- connector
|
|
12
|
-
|
|
13
|
-
## Customer one-command flow
|
|
14
|
-
|
|
10
|
+
- `install` runs `doctor + auto-fix + optional gateway restart + connector service start`
|
|
11
|
+
- by default connector runs in background service, so CMD can be closed after install
|
|
12
|
+
|
|
13
|
+
## Customer one-command flow
|
|
14
|
+
|
|
15
15
|
Published package usage:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npx -y mateclaw-openclaw-plugin install
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Local repo usage:
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
cd mateclaw-openclaw-plugin
|
|
25
|
-
npm install
|
|
26
|
-
node ./src/cli.mjs install --chat-mode gateway-session --session-key agent:main:main
|
|
18
|
+
npx -y mateclaw-openclaw-plugin install
|
|
27
19
|
```
|
|
28
20
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`
|
|
35
|
-
|
|
36
|
-
## What `install` auto-handles
|
|
21
|
+
This preset command auto-uses:
|
|
22
|
+
- `chat-mode=gateway-session`
|
|
23
|
+
- `session-key=agent:main:main`
|
|
24
|
+
- built-in reverse tunnel
|
|
25
|
+
- tunnel target `xxx@xxx`
|
|
26
|
+
- public bind URL `https://xxx`
|
|
37
27
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
- Auto-backs up existing `openclaw.json` before writing
|
|
41
|
-
- Checks and auto-creates device identity (`~/.openclaw/identity/device.json`) when missing
|
|
42
|
-
- Checks and syncs device auth token (`~/.openclaw/identity/device-auth.json`) when missing
|
|
43
|
-
- Auto-selects current LAN IPv4
|
|
44
|
-
- Auto-switches connector port when preferred port is occupied
|
|
45
|
-
- Optionally restarts gateway (`openclaw gateway restart`)
|
|
46
|
-
- Starts connector and prints binding QR
|
|
28
|
+
You will be prompted for tunnel password with hidden input.
|
|
29
|
+
If you only want LAN (no tunnel): `npx -y mateclaw-openclaw-plugin install --lan-only`
|
|
47
30
|
|
|
48
|
-
|
|
31
|
+
Background service commands:
|
|
49
32
|
|
|
50
33
|
```bash
|
|
34
|
+
npx -y mateclaw-openclaw-plugin service-status
|
|
35
|
+
npx -y mateclaw-openclaw-plugin service-stop
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Local repo usage:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cd mateclaw-openclaw-plugin
|
|
42
|
+
npm install
|
|
43
|
+
node ./src/cli.mjs install --chat-mode gateway-session --session-key agent:main:main
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
After startup:
|
|
47
|
+
|
|
48
|
+
1. Scan the OpenClaw-side QR code shown by the installer in MateClaw app.
|
|
49
|
+
2. Start text chat directly in app.
|
|
50
|
+
3. Optional desktop same-session view:
|
|
51
|
+
`http://127.0.0.1:18789/chat?session=agent%3Amain%3Amain`
|
|
52
|
+
|
|
53
|
+
## What `install` auto-handles
|
|
54
|
+
|
|
55
|
+
- Checks OpenClaw CLI version and gateway health/status
|
|
56
|
+
- Checks and repairs `~/.openclaw/openclaw.json` (`gateway.mode`, `gateway.auth.mode`, token/password)
|
|
57
|
+
- Auto-backs up existing `openclaw.json` before writing
|
|
58
|
+
- Checks and auto-creates device identity (`~/.openclaw/identity/device.json`) when missing
|
|
59
|
+
- Checks and syncs device auth token (`~/.openclaw/identity/device-auth.json`) when missing
|
|
60
|
+
- Auto-selects current LAN IPv4
|
|
61
|
+
- Auto-switches connector port when preferred port is occupied
|
|
62
|
+
- Optionally restarts gateway (`openclaw gateway restart`)
|
|
63
|
+
- Starts connector and prints binding QR
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
```bash
|
|
51
68
|
node ./src/cli.mjs doctor
|
|
52
69
|
node ./src/cli.mjs install
|
|
53
70
|
node ./src/cli.mjs connect
|
|
54
71
|
node ./src/cli.mjs login install # alias of install
|
|
72
|
+
node ./src/cli.mjs service-status
|
|
73
|
+
node ./src/cli.mjs service-stop
|
|
55
74
|
```
|
|
56
|
-
|
|
57
|
-
## Useful options
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
node ./src/cli.mjs install \
|
|
61
|
-
--openclaw-url http://127.0.0.1:18789 \
|
|
62
|
-
--chat-mode gateway-session \
|
|
63
|
-
--session-key agent:main:main \
|
|
64
|
-
--port 18890
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
- `--openclaw-bin <cmd>`: override OpenClaw CLI command
|
|
68
|
-
- `--config-path <path>`: override openclaw config path
|
|
75
|
+
|
|
76
|
+
## Useful options
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
node ./src/cli.mjs install \
|
|
80
|
+
--openclaw-url http://127.0.0.1:18789 \
|
|
81
|
+
--chat-mode gateway-session \
|
|
82
|
+
--session-key agent:main:main \
|
|
83
|
+
--port 18890
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- `--openclaw-bin <cmd>`: override OpenClaw CLI command
|
|
87
|
+
- `--config-path <path>`: override openclaw config path
|
|
69
88
|
- `--skip-gateway-restart`: skip `openclaw gateway restart`
|
|
70
89
|
- `--skip-connector`: only run setup, do not start connector
|
|
90
|
+
- `--foreground`: run connector in current CMD (do not use background service)
|
|
71
91
|
- `--dry-run`: print planned fixes without writing files
|
|
72
92
|
- `--lan-host <ip>`: pin LAN IP manually
|
|
93
|
+
- `--public-base-url <url>`: override QR baseUrl with public URL
|
|
94
|
+
- `--tunnel`: enable reverse tunnel
|
|
95
|
+
- `--lan-only`: disable tunnel and force LAN-only QR/baseUrl
|
|
96
|
+
- `--tunnel-client <builtin|ssh|nssh>`: choose tunnel client (default: `builtin`)
|
|
97
|
+
- `--tunnel-user / --tunnel-host`: tunnel account and host
|
|
98
|
+
- `--tunnel-password`: required by `builtin` and `nssh`
|
|
73
99
|
|
|
74
|
-
##
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
npm run doctor
|
|
78
|
-
npm run install:local
|
|
79
|
-
npm run connect
|
|
80
|
-
npm run check:publish
|
|
81
|
-
npm run pack:dry-run
|
|
82
|
-
npm run publish:dry-run
|
|
83
|
-
```
|
|
100
|
+
## Intranet tunnel (internal provider)
|
|
84
101
|
|
|
85
|
-
|
|
102
|
+
Example (one-command, no extra tunnel client install, use `builtin`):
|
|
86
103
|
|
|
87
104
|
```bash
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
node ./src/cli.mjs install \
|
|
106
|
+
--chat-mode gateway-session \
|
|
107
|
+
--session-key agent:main:main \
|
|
108
|
+
--tunnel \
|
|
109
|
+
--tunnel-client builtin \
|
|
110
|
+
--tunnel-user xxx \
|
|
111
|
+
--tunnel-host xxx \
|
|
112
|
+
--tunnel-remote-port 80 \
|
|
113
|
+
--public-base-url https://xxx
|
|
92
114
|
```
|
|
93
115
|
|
|
94
|
-
|
|
116
|
+
If `--tunnel-password` is omitted, CLI will ask for hidden password input interactively.
|
|
117
|
+
|
|
118
|
+
Alternative external client forms:
|
|
95
119
|
|
|
96
120
|
```bash
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
npm run publish:token
|
|
121
|
+
ssh -R 80:127.0.0.1:18890 xxx@xxx
|
|
122
|
+
nssh -R 80:127.0.0.1:18890 xxx@xxx --passwd <password>
|
|
100
123
|
```
|
|
124
|
+
|
|
125
|
+
## Security note (next phase)
|
|
126
|
+
|
|
127
|
+
Current simple mode still needs customer-side tunnel credentials.
|
|
128
|
+
Planned upgrade is backend-issued short-lived tunnel tickets, so no long-term shared password is exposed to customers.
|
|
129
|
+
|
|
130
|
+
## Development scripts
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npm run doctor
|
|
134
|
+
npm run install:local
|
|
135
|
+
npm run connect
|
|
136
|
+
npm run check:publish
|
|
137
|
+
npm run pack:dry-run
|
|
138
|
+
npm run publish:dry-run
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Publish
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm login
|
|
145
|
+
npm run check:publish
|
|
146
|
+
npm run publish:dry-run
|
|
147
|
+
npm run publish:public
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Use Automation Token (no OTP on publish):
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# PowerShell
|
|
154
|
+
$env:NPM_TOKEN = "npm_xxx_your_automation_token"
|
|
155
|
+
npm run publish:token
|
|
156
|
+
```
|
package/package.json
CHANGED
|
@@ -1,37 +1,38 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "mateclaw-openclaw-plugin",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"private": false,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"description": "Local OpenClaw connector for MateClaw customer demos",
|
|
7
|
-
"license": "MIT",
|
|
8
|
-
"bin": {
|
|
9
|
-
"mateclaw-openclaw-plugin": "src/cli.mjs"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"src",
|
|
13
|
-
"README.md",
|
|
14
|
-
"LICENSE"
|
|
15
|
-
],
|
|
16
|
-
"publishConfig": {
|
|
17
|
-
"access": "public"
|
|
18
|
-
},
|
|
19
|
-
"scripts": {
|
|
20
|
-
"doctor": "node ./src/cli.mjs doctor",
|
|
21
|
-
"install:local": "node ./src/cli.mjs install",
|
|
22
|
-
"connect": "node ./src/cli.mjs connect",
|
|
23
|
-
"check:publish": "node ./scripts/prepublish-check.mjs",
|
|
24
|
-
"pack:dry-run": "npm pack --dry-run",
|
|
25
|
-
"publish:dry-run": "npm publish --dry-run --access public",
|
|
26
|
-
"publish:public": "npm publish --access public",
|
|
27
|
-
"publish:token": "node ./scripts/publish-with-token.mjs",
|
|
28
|
-
"prepublishOnly": "npm run check:publish"
|
|
29
|
-
},
|
|
30
|
-
"dependencies": {
|
|
31
|
-
"qrcode-terminal": "^0.12.0",
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "mateclaw-openclaw-plugin",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Local OpenClaw connector for MateClaw customer demos",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"bin": {
|
|
9
|
+
"mateclaw-openclaw-plugin": "src/cli.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"doctor": "node ./src/cli.mjs doctor",
|
|
21
|
+
"install:local": "node ./src/cli.mjs install",
|
|
22
|
+
"connect": "node ./src/cli.mjs connect",
|
|
23
|
+
"check:publish": "node ./scripts/prepublish-check.mjs",
|
|
24
|
+
"pack:dry-run": "npm pack --dry-run",
|
|
25
|
+
"publish:dry-run": "npm publish --dry-run --access public",
|
|
26
|
+
"publish:public": "npm publish --access public",
|
|
27
|
+
"publish:token": "node ./scripts/publish-with-token.mjs",
|
|
28
|
+
"prepublishOnly": "npm run check:publish"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"qrcode-terminal": "^0.12.0",
|
|
32
|
+
"ssh2": "^1.17.0",
|
|
33
|
+
"ws": "^8.18.3"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
|
-
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import http from 'node:http';
|
|
7
7
|
import https from 'node:https';
|
|
8
8
|
import net from 'node:net';
|
|
9
9
|
import os from 'node:os';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
import
|
|
11
|
+
import readline from 'node:readline';
|
|
12
|
+
import { URL, fileURLToPath } from 'node:url';
|
|
12
13
|
import qrcode from 'qrcode-terminal';
|
|
14
|
+
import { Client as SshClient } from 'ssh2';
|
|
13
15
|
import { WebSocket } from 'ws';
|
|
14
16
|
|
|
15
17
|
const DEFAULT_OPENCLAW_URL = 'http://127.0.0.1:18789';
|
|
@@ -27,8 +29,18 @@ const DEFAULT_PORT = 18890;
|
|
|
27
29
|
const DEFAULT_AGENT_ID = 'main';
|
|
28
30
|
const DEFAULT_EXPIRES_MINUTES = 720;
|
|
29
31
|
const DEFAULT_UPSTREAM_TIMEOUT_MS = 180000;
|
|
30
|
-
const PAYLOAD_TYPE = '
|
|
32
|
+
const PAYLOAD_TYPE = 'mateclaw_openclaw_plugin';
|
|
31
33
|
const DEFAULT_OPENCLAW_BIN = process.platform === 'win32' ? 'openclaw.cmd' : 'openclaw';
|
|
34
|
+
const DEFAULT_TUNNEL_CLIENT = 'builtin';
|
|
35
|
+
const DEFAULT_TUNNEL_SERVER_PORT = 22;
|
|
36
|
+
const DEFAULT_TUNNEL_REMOTE_PORT = 80;
|
|
37
|
+
const DEFAULT_TUNNEL_LOCAL_HOST = '127.0.0.1';
|
|
38
|
+
const DEFAULT_PRESET_TUNNEL_ENABLED = true;
|
|
39
|
+
const DEFAULT_PRESET_TUNNEL_USER = '6111605006';
|
|
40
|
+
const DEFAULT_PRESET_TUNNEL_HOST = 'gz2.neiwangyun.net';
|
|
41
|
+
const DEFAULT_PRESET_TUNNEL_PASSWORD = 'skg5gm';
|
|
42
|
+
const DEFAULT_PRESET_PUBLIC_BASE_URL = 'https://6hjduygz2.neiwangyun.net';
|
|
43
|
+
const CURRENT_CLI_PATH = fileURLToPath(import.meta.url);
|
|
32
44
|
|
|
33
45
|
async function main() {
|
|
34
46
|
const rawArgs = process.argv.slice(2);
|
|
@@ -37,7 +49,7 @@ async function main() {
|
|
|
37
49
|
|
|
38
50
|
switch (resolved.command) {
|
|
39
51
|
case 'connect': {
|
|
40
|
-
const config = buildConfig(options);
|
|
52
|
+
const config = await buildConfig(options);
|
|
41
53
|
await startConnector(config);
|
|
42
54
|
return;
|
|
43
55
|
}
|
|
@@ -49,6 +61,14 @@ async function main() {
|
|
|
49
61
|
await runDoctor(options);
|
|
50
62
|
return;
|
|
51
63
|
}
|
|
64
|
+
case 'service-stop': {
|
|
65
|
+
await runServiceStop();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
case 'service-status': {
|
|
69
|
+
await runServiceStatus();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
52
72
|
case 'help':
|
|
53
73
|
printHelp();
|
|
54
74
|
return;
|
|
@@ -79,8 +99,10 @@ function printHelp() {
|
|
|
79
99
|
console.log('');
|
|
80
100
|
console.log('Commands:');
|
|
81
101
|
console.log(' install One-command setup: check OpenClaw, auto-fix config, restart gateway, start connector');
|
|
82
|
-
console.log(' login install Alias of install
|
|
102
|
+
console.log(' login install Alias of install');
|
|
83
103
|
console.log(' connect Start connector directly (no config repair)');
|
|
104
|
+
console.log(' service-stop Stop background connector service started by install');
|
|
105
|
+
console.log(' service-status Show background connector service status');
|
|
84
106
|
console.log(' doctor Check OpenClaw and local config health');
|
|
85
107
|
console.log('');
|
|
86
108
|
console.log('Shared options (connect/install):');
|
|
@@ -97,13 +119,28 @@ function printHelp() {
|
|
|
97
119
|
console.log(' --upstream-timeout-ms <ms> Upstream OpenClaw request timeout');
|
|
98
120
|
console.log(' --chat-mode <mode> gateway-session | http-proxy');
|
|
99
121
|
console.log(' --session-key <key> Fixed Gateway session key (default: agent:<agent-id>:main)');
|
|
122
|
+
console.log(' --public-base-url <url> Override QR baseUrl with a public URL');
|
|
123
|
+
console.log(' --tunnel Enable reverse tunnel (ssh/nssh)');
|
|
124
|
+
console.log(' --lan-only Force LAN-only mode (disable tunnel/public URL)');
|
|
125
|
+
console.log(' --tunnel-client <name> builtin | ssh | nssh (default: builtin)');
|
|
126
|
+
console.log(' builtin uses embedded SSH reverse tunnel (no nssh install needed)');
|
|
127
|
+
console.log(' --tunnel-bin <cmd> Tunnel client command path');
|
|
128
|
+
console.log(' --tunnel-user <user> Tunnel account user');
|
|
129
|
+
console.log(' --tunnel-host <host> Tunnel server host');
|
|
130
|
+
console.log(' --tunnel-server-port <n> SSH service port (default: 22)');
|
|
131
|
+
console.log(' --tunnel-remote-port <n> Public exposed port (default: 80)');
|
|
132
|
+
console.log(' --tunnel-password <pwd> tunnel password for builtin/nssh (or set MATECLAW_TUNNEL_PASSWORD)');
|
|
100
133
|
console.log('');
|
|
101
134
|
console.log('Install/Doctor options:');
|
|
102
135
|
console.log(' --openclaw-bin <cmd> OpenClaw CLI command (default: openclaw/openclaw.cmd)');
|
|
103
136
|
console.log(' --config-path <path> Override openclaw.json path');
|
|
104
137
|
console.log(' --skip-gateway-restart Skip openclaw gateway restart after config updates');
|
|
105
138
|
console.log(' --skip-connector Only setup config, do not start local connector');
|
|
139
|
+
console.log(' --foreground Keep connector in current CMD (no background service)');
|
|
106
140
|
console.log(' --dry-run Validate and print changes without writing files');
|
|
141
|
+
console.log('');
|
|
142
|
+
console.log('Quick start (preset tunnel defaults + hidden password prompt):');
|
|
143
|
+
console.log(' npx -y mateclaw-openclaw-plugin install');
|
|
107
144
|
}
|
|
108
145
|
|
|
109
146
|
function parseArgs(args) {
|
|
@@ -224,6 +261,7 @@ function runOpenClawCommand(bin, args, timeoutMs = 20000) {
|
|
|
224
261
|
encoding: 'utf8',
|
|
225
262
|
timeout: timeoutMs,
|
|
226
263
|
shell: process.platform === 'win32',
|
|
264
|
+
windowsHide: true,
|
|
227
265
|
});
|
|
228
266
|
const stdout = `${result.stdout || ''}`.trim();
|
|
229
267
|
const stderr = `${result.stderr || ''}`.trim();
|
|
@@ -263,6 +301,41 @@ function summarizeCommandFailure(result) {
|
|
|
263
301
|
return detail || `exit code ${result.status}`;
|
|
264
302
|
}
|
|
265
303
|
|
|
304
|
+
function isCommandAvailable(commandName) {
|
|
305
|
+
const name = `${commandName || ''}`.trim();
|
|
306
|
+
if (!name) return false;
|
|
307
|
+
const probeTool = process.platform === 'win32' ? 'where' : 'which';
|
|
308
|
+
const probe = runOpenClawCommand(probeTool, [name], 8000);
|
|
309
|
+
return probe.ok && `${probe.stdout || ''}`.trim().length > 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function readSecretFromPrompt(promptText) {
|
|
313
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
314
|
+
return '';
|
|
315
|
+
}
|
|
316
|
+
const rl = readline.createInterface({
|
|
317
|
+
input: process.stdin,
|
|
318
|
+
output: process.stdout,
|
|
319
|
+
terminal: true,
|
|
320
|
+
});
|
|
321
|
+
const originalWrite = rl._writeToOutput;
|
|
322
|
+
rl._writeToOutput = function writeMaskedOutput(stringToWrite) {
|
|
323
|
+
if (rl.stdoutMuted) {
|
|
324
|
+
rl.output.write('*');
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
originalWrite.call(rl, stringToWrite);
|
|
328
|
+
};
|
|
329
|
+
rl.stdoutMuted = true;
|
|
330
|
+
const secret = await new Promise((resolve) => {
|
|
331
|
+
rl.question(promptText, (answer) => resolve(`${answer || ''}`.trim()));
|
|
332
|
+
});
|
|
333
|
+
rl.stdoutMuted = false;
|
|
334
|
+
rl.close();
|
|
335
|
+
process.stdout.write('\n');
|
|
336
|
+
return secret;
|
|
337
|
+
}
|
|
338
|
+
|
|
266
339
|
function extractGatewayTokenFromConfig(config) {
|
|
267
340
|
const mode = `${config?.gateway?.auth?.mode || ''}`.trim().toLowerCase();
|
|
268
341
|
if (mode === 'password') {
|
|
@@ -278,6 +351,11 @@ function maskToken(token) {
|
|
|
278
351
|
return `${text.slice(0, 4)}...${text.slice(-2)}`;
|
|
279
352
|
}
|
|
280
353
|
|
|
354
|
+
function maskTunnelPasswordInCommand(commandText) {
|
|
355
|
+
if (!commandText) return '';
|
|
356
|
+
return commandText.replace(/(--passwd\s+)([^\s]+)/i, '$1***');
|
|
357
|
+
}
|
|
358
|
+
|
|
281
359
|
function parsePortOrDefault(raw, fallback) {
|
|
282
360
|
const parsed = Number.parseInt(`${raw ?? ''}`, 10);
|
|
283
361
|
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
@@ -911,6 +989,135 @@ async function runDoctor(options) {
|
|
|
911
989
|
}
|
|
912
990
|
}
|
|
913
991
|
|
|
992
|
+
function buildConnectArgsFromConfig(config) {
|
|
993
|
+
const args = [
|
|
994
|
+
'connect',
|
|
995
|
+
'--openclaw-url', `${config.openclawUrl}`,
|
|
996
|
+
'--chat-path', `${config.chatPath}`,
|
|
997
|
+
'--host', `${config.listenHost}`,
|
|
998
|
+
'--port', `${config.port}`,
|
|
999
|
+
'--lan-host', `${config.lanHost}`,
|
|
1000
|
+
'--agent-id', `${config.agentId}`,
|
|
1001
|
+
'--name', `${config.name}`,
|
|
1002
|
+
'--token', `${config.token}`,
|
|
1003
|
+
'--expires-minutes', `${Math.max(1, Math.ceil((config.expiresAt.getTime() - Date.now()) / 60000))}`,
|
|
1004
|
+
'--upstream-timeout-ms', `${config.upstreamTimeoutMs}`,
|
|
1005
|
+
'--chat-mode', `${config.chatMode}`,
|
|
1006
|
+
'--session-key', `${config.sessionKey}`,
|
|
1007
|
+
];
|
|
1008
|
+
|
|
1009
|
+
if (config.upstreamToken) {
|
|
1010
|
+
args.push('--upstream-token', `${config.upstreamToken}`);
|
|
1011
|
+
}
|
|
1012
|
+
if (config.lanOnly) {
|
|
1013
|
+
args.push('--lan-only');
|
|
1014
|
+
}
|
|
1015
|
+
if (config.publicBaseUrl) {
|
|
1016
|
+
args.push('--public-base-url', `${config.publicBaseUrl}`);
|
|
1017
|
+
}
|
|
1018
|
+
if (config.tunnel?.enabled) {
|
|
1019
|
+
args.push('--tunnel');
|
|
1020
|
+
args.push('--tunnel-client', `${config.tunnel.client}`);
|
|
1021
|
+
if (config.tunnel.bin) args.push('--tunnel-bin', `${config.tunnel.bin}`);
|
|
1022
|
+
if (config.tunnel.user) args.push('--tunnel-user', `${config.tunnel.user}`);
|
|
1023
|
+
if (config.tunnel.host) args.push('--tunnel-host', `${config.tunnel.host}`);
|
|
1024
|
+
if (config.tunnel.password) args.push('--tunnel-password', `${config.tunnel.password}`);
|
|
1025
|
+
if (config.tunnel.serverPort) args.push('--tunnel-server-port', `${config.tunnel.serverPort}`);
|
|
1026
|
+
if (config.tunnel.remotePort) args.push('--tunnel-remote-port', `${config.tunnel.remotePort}`);
|
|
1027
|
+
}
|
|
1028
|
+
return args;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
async function startConnectorService(config) {
|
|
1032
|
+
const existing = readServiceRecord();
|
|
1033
|
+
if (existing?.pid && isPidRunning(existing.pid)) {
|
|
1034
|
+
return {
|
|
1035
|
+
ok: false,
|
|
1036
|
+
message: `Connector service is already running (PID ${existing.pid}). Run service-stop first.`,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const { baseDir, outLogPath, errLogPath } = resolveServicePaths();
|
|
1041
|
+
fs.mkdirSync(baseDir, { recursive: true });
|
|
1042
|
+
const outFd = fs.openSync(outLogPath, 'a');
|
|
1043
|
+
const errFd = fs.openSync(errLogPath, 'a');
|
|
1044
|
+
const args = buildConnectArgsFromConfig(config);
|
|
1045
|
+
const child = spawn(process.execPath, [CURRENT_CLI_PATH, ...args], {
|
|
1046
|
+
detached: true,
|
|
1047
|
+
windowsHide: true,
|
|
1048
|
+
stdio: ['ignore', outFd, errFd],
|
|
1049
|
+
});
|
|
1050
|
+
child.unref();
|
|
1051
|
+
fs.closeSync(outFd);
|
|
1052
|
+
fs.closeSync(errFd);
|
|
1053
|
+
|
|
1054
|
+
writeServiceRecord({
|
|
1055
|
+
pid: child.pid,
|
|
1056
|
+
startedAt: new Date().toISOString(),
|
|
1057
|
+
sessionKey: config.sessionKey,
|
|
1058
|
+
baseUrl: config.baseUrl,
|
|
1059
|
+
localBaseUrl: config.localBaseUrl,
|
|
1060
|
+
outLogPath,
|
|
1061
|
+
errLogPath,
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
return {
|
|
1065
|
+
ok: true,
|
|
1066
|
+
pid: child.pid,
|
|
1067
|
+
outLogPath,
|
|
1068
|
+
errLogPath,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
async function runServiceStop() {
|
|
1073
|
+
const record = readServiceRecord();
|
|
1074
|
+
if (!record?.pid) {
|
|
1075
|
+
console.log('[PASS] No background connector service record found.');
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const pid = Number.parseInt(`${record.pid || ''}`, 10);
|
|
1079
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
1080
|
+
deleteServiceRecord();
|
|
1081
|
+
console.log('[WARN] Service PID record is invalid. Record removed.');
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (!isPidRunning(pid)) {
|
|
1085
|
+
deleteServiceRecord();
|
|
1086
|
+
console.log(`[PASS] Connector service PID ${pid} is not running. Record removed.`);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
try {
|
|
1090
|
+
process.kill(pid);
|
|
1091
|
+
deleteServiceRecord();
|
|
1092
|
+
console.log(`[PASS] Stopped connector service (PID ${pid}).`);
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
throw new Error(`Failed to stop connector service PID ${pid}: ${error?.message || error}`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
async function runServiceStatus() {
|
|
1099
|
+
const record = readServiceRecord();
|
|
1100
|
+
if (!record?.pid) {
|
|
1101
|
+
console.log('[PASS] Connector service is not running (no record).');
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
const running = isPidRunning(record.pid);
|
|
1105
|
+
console.log('');
|
|
1106
|
+
console.log('MateClaw Connector Service Status');
|
|
1107
|
+
console.log('================================');
|
|
1108
|
+
console.log(`PID : ${record.pid}`);
|
|
1109
|
+
console.log(`Running : ${running ? 'yes' : 'no'}`);
|
|
1110
|
+
console.log(`StartedAt : ${record.startedAt || '-'}`);
|
|
1111
|
+
console.log(`Session : ${record.sessionKey || '-'}`);
|
|
1112
|
+
console.log(`Base URL : ${record.baseUrl || '-'}`);
|
|
1113
|
+
console.log(`Out Log : ${record.outLogPath || '-'}`);
|
|
1114
|
+
console.log(`Err Log : ${record.errLogPath || '-'}`);
|
|
1115
|
+
console.log('');
|
|
1116
|
+
if (!running) {
|
|
1117
|
+
console.log('[WARN] Service record exists but process is not running. Consider service-stop to clean record.');
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
914
1121
|
async function runInstall(options) {
|
|
915
1122
|
const dryRun = optionEnabled(options, 'dry-run');
|
|
916
1123
|
const skipGatewayRestart = optionEnabled(options, 'skip-gateway-restart');
|
|
@@ -928,6 +1135,35 @@ async function runInstall(options) {
|
|
|
928
1135
|
printInstallFixSummary(fixResult, dryRun);
|
|
929
1136
|
|
|
930
1137
|
const installOptions = { ...options };
|
|
1138
|
+
const lanOnly = optionEnabled(installOptions, 'lan-only');
|
|
1139
|
+
if (installOptions['chat-mode'] == null) {
|
|
1140
|
+
installOptions['chat-mode'] = DEFAULT_CHAT_MODE;
|
|
1141
|
+
}
|
|
1142
|
+
if (installOptions['session-key'] == null) {
|
|
1143
|
+
installOptions['session-key'] = DEFAULT_SESSION_KEY;
|
|
1144
|
+
}
|
|
1145
|
+
if (!lanOnly && installOptions.tunnel == null && DEFAULT_PRESET_TUNNEL_ENABLED) {
|
|
1146
|
+
installOptions.tunnel = 'true';
|
|
1147
|
+
}
|
|
1148
|
+
if (installOptions['tunnel-client'] == null) {
|
|
1149
|
+
installOptions['tunnel-client'] = DEFAULT_TUNNEL_CLIENT;
|
|
1150
|
+
}
|
|
1151
|
+
if (installOptions['tunnel-user'] == null) {
|
|
1152
|
+
installOptions['tunnel-user'] = DEFAULT_PRESET_TUNNEL_USER;
|
|
1153
|
+
}
|
|
1154
|
+
if (installOptions['tunnel-host'] == null) {
|
|
1155
|
+
installOptions['tunnel-host'] = DEFAULT_PRESET_TUNNEL_HOST;
|
|
1156
|
+
}
|
|
1157
|
+
if (installOptions['tunnel-password'] == null) {
|
|
1158
|
+
installOptions['tunnel-password'] = DEFAULT_PRESET_TUNNEL_PASSWORD;
|
|
1159
|
+
}
|
|
1160
|
+
if (!lanOnly && installOptions['public-base-url'] == null) {
|
|
1161
|
+
installOptions['public-base-url'] = DEFAULT_PRESET_PUBLIC_BASE_URL;
|
|
1162
|
+
}
|
|
1163
|
+
if (lanOnly) {
|
|
1164
|
+
installOptions.tunnel = 'false';
|
|
1165
|
+
delete installOptions['public-base-url'];
|
|
1166
|
+
}
|
|
931
1167
|
|
|
932
1168
|
const openclawUrl = stripTrailingSlash(
|
|
933
1169
|
installOptions['openclaw-url'] ||
|
|
@@ -1024,11 +1260,27 @@ async function runInstall(options) {
|
|
|
1024
1260
|
return;
|
|
1025
1261
|
}
|
|
1026
1262
|
|
|
1027
|
-
const config = buildConfig(installOptions);
|
|
1028
|
-
|
|
1263
|
+
const config = await buildConfig(installOptions);
|
|
1264
|
+
const foreground = optionEnabled(installOptions, 'foreground');
|
|
1265
|
+
if (foreground) {
|
|
1266
|
+
console.log('[WARN] Running connector in foreground mode (--foreground). Keep this CMD open.');
|
|
1267
|
+
await startConnector(config);
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const serviceStart = await startConnectorService(config);
|
|
1272
|
+
if (!serviceStart.ok) {
|
|
1273
|
+
throw new Error(serviceStart.message || 'Failed to start background connector service.');
|
|
1274
|
+
}
|
|
1275
|
+
console.log(`[PASS] Connector service started in background (PID ${serviceStart.pid}).`);
|
|
1276
|
+
console.log('[PASS] You can now close this CMD window.');
|
|
1277
|
+
console.log(`[PASS] Service logs: ${serviceStart.outLogPath}`);
|
|
1278
|
+
console.log('[PASS] Stop service: npx -y mateclaw-openclaw-plugin service-stop');
|
|
1279
|
+
console.log('[PASS] Service status: npx -y mateclaw-openclaw-plugin service-status');
|
|
1280
|
+
printBanner(config);
|
|
1029
1281
|
}
|
|
1030
1282
|
|
|
1031
|
-
function buildConfig(options) {
|
|
1283
|
+
async function buildConfig(options) {
|
|
1032
1284
|
const openclawUrl = stripTrailingSlash(
|
|
1033
1285
|
options['openclaw-url'] ||
|
|
1034
1286
|
process.env.OPENCLAW_BASE_URL ||
|
|
@@ -1057,6 +1309,10 @@ function buildConfig(options) {
|
|
|
1057
1309
|
process.env.MATECLAW_CONNECTOR_LAN_HOST ||
|
|
1058
1310
|
lanCandidates[0]?.address ||
|
|
1059
1311
|
'';
|
|
1312
|
+
const lanOnly = optionEnabled(options, 'lan-only');
|
|
1313
|
+
const publicBaseUrl = normalizePublicBaseUrl(
|
|
1314
|
+
options['public-base-url'] || process.env.MATECLAW_PUBLIC_BASE_URL || '',
|
|
1315
|
+
);
|
|
1060
1316
|
const agentId =
|
|
1061
1317
|
options['agent-id'] || process.env.MATECLAW_AGENT_ID || DEFAULT_AGENT_ID;
|
|
1062
1318
|
const sessionKey = normalizeSessionKey(
|
|
@@ -1085,6 +1341,22 @@ function buildConfig(options) {
|
|
|
1085
1341
|
process.env.OPENCLAW_UPSTREAM_TOKEN ||
|
|
1086
1342
|
detectLocalDeviceAuthToken() ||
|
|
1087
1343
|
detectLocalGatewayToken();
|
|
1344
|
+
const tunnelEnabled = !lanOnly && optionEnabled(options, 'tunnel');
|
|
1345
|
+
const tunnelClient = normalizeTunnelClient(
|
|
1346
|
+
options['tunnel-client'] || process.env.MATECLAW_TUNNEL_CLIENT || '',
|
|
1347
|
+
);
|
|
1348
|
+
const tunnelBin = `${options['tunnel-bin'] || process.env.MATECLAW_TUNNEL_BIN || tunnelClient}`.trim();
|
|
1349
|
+
const tunnelUser = `${options['tunnel-user'] || process.env.MATECLAW_TUNNEL_USER || ''}`.trim();
|
|
1350
|
+
const tunnelHost = `${options['tunnel-host'] || process.env.MATECLAW_TUNNEL_HOST || ''}`.trim();
|
|
1351
|
+
let tunnelPassword = `${options['tunnel-password'] || process.env.MATECLAW_TUNNEL_PASSWORD || ''}`.trim();
|
|
1352
|
+
const tunnelServerPort = parsePortOrDefault(
|
|
1353
|
+
options['tunnel-server-port'] || process.env.MATECLAW_TUNNEL_SERVER_PORT || `${DEFAULT_TUNNEL_SERVER_PORT}`,
|
|
1354
|
+
DEFAULT_TUNNEL_SERVER_PORT,
|
|
1355
|
+
);
|
|
1356
|
+
const tunnelRemotePort = parsePortOrDefault(
|
|
1357
|
+
options['tunnel-remote-port'] || process.env.MATECLAW_TUNNEL_REMOTE_PORT || `${DEFAULT_TUNNEL_REMOTE_PORT}`,
|
|
1358
|
+
DEFAULT_TUNNEL_REMOTE_PORT,
|
|
1359
|
+
);
|
|
1088
1360
|
|
|
1089
1361
|
if (!Number.isFinite(port) || port <= 0) {
|
|
1090
1362
|
throw new Error('Invalid port.');
|
|
@@ -1100,9 +1372,29 @@ function buildConfig(options) {
|
|
|
1100
1372
|
'Unable to detect a LAN host automatically. Pass --lan-host or set MATECLAW_CONNECTOR_LAN_HOST.',
|
|
1101
1373
|
);
|
|
1102
1374
|
}
|
|
1375
|
+
if (tunnelEnabled && (!tunnelUser || !tunnelHost)) {
|
|
1376
|
+
throw new Error(
|
|
1377
|
+
'Tunnel enabled but tunnel target is incomplete. Pass --tunnel-user and --tunnel-host.',
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
if (tunnelEnabled && tunnelClient !== 'builtin' && !isCommandAvailable(tunnelBin)) {
|
|
1381
|
+
throw new Error(
|
|
1382
|
+
`Tunnel client not found: ${tunnelBin}. Install it or set --tunnel-bin with a valid command.`,
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
if (tunnelEnabled && (tunnelClient === 'nssh' || tunnelClient === 'builtin') && !tunnelPassword) {
|
|
1386
|
+
tunnelPassword = await readSecretFromPrompt('Tunnel password (hidden input): ');
|
|
1387
|
+
}
|
|
1388
|
+
if (tunnelEnabled && (tunnelClient === 'nssh' || tunnelClient === 'builtin') && !tunnelPassword) {
|
|
1389
|
+
throw new Error(
|
|
1390
|
+
`Tunnel client ${tunnelClient} requires --tunnel-password (or MATECLAW_TUNNEL_PASSWORD).`,
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1103
1393
|
|
|
1104
1394
|
const expiresAt = new Date(Date.now() + expiresMinutes * 60 * 1000);
|
|
1105
|
-
const
|
|
1395
|
+
const localBaseUrl = `http://${lanHost}:${port}`;
|
|
1396
|
+
const effectivePublicBaseUrl = tunnelEnabled ? publicBaseUrl : '';
|
|
1397
|
+
const baseUrl = effectivePublicBaseUrl || localBaseUrl;
|
|
1106
1398
|
|
|
1107
1399
|
return {
|
|
1108
1400
|
openclawUrl,
|
|
@@ -1119,8 +1411,23 @@ function buildConfig(options) {
|
|
|
1119
1411
|
upstreamTimeoutMs,
|
|
1120
1412
|
upstreamToken,
|
|
1121
1413
|
lanCandidates,
|
|
1414
|
+
lanOnly,
|
|
1415
|
+
localBaseUrl,
|
|
1416
|
+
publicBaseUrl: effectivePublicBaseUrl,
|
|
1122
1417
|
baseUrl,
|
|
1123
1418
|
acceptedChatPaths,
|
|
1419
|
+
tunnel: {
|
|
1420
|
+
enabled: tunnelEnabled,
|
|
1421
|
+
client: tunnelClient,
|
|
1422
|
+
bin: tunnelBin,
|
|
1423
|
+
user: tunnelUser,
|
|
1424
|
+
host: tunnelHost,
|
|
1425
|
+
password: tunnelPassword,
|
|
1426
|
+
serverPort: tunnelServerPort,
|
|
1427
|
+
remotePort: tunnelRemotePort,
|
|
1428
|
+
localHost: DEFAULT_TUNNEL_LOCAL_HOST,
|
|
1429
|
+
localPort: port,
|
|
1430
|
+
},
|
|
1124
1431
|
qrPayload: {
|
|
1125
1432
|
type: PAYLOAD_TYPE,
|
|
1126
1433
|
name,
|
|
@@ -1188,6 +1495,254 @@ function loadLocalDeviceIdentity() {
|
|
|
1188
1495
|
}
|
|
1189
1496
|
}
|
|
1190
1497
|
|
|
1498
|
+
function buildTunnelLaunchPlan(config) {
|
|
1499
|
+
const tunnel = config.tunnel;
|
|
1500
|
+
if (!tunnel?.enabled) return null;
|
|
1501
|
+
|
|
1502
|
+
const routeSpec = `${tunnel.remotePort}:${tunnel.localHost}:${tunnel.localPort}`;
|
|
1503
|
+
const destination = `${tunnel.user}@${tunnel.host}`;
|
|
1504
|
+
const command = tunnel.bin || tunnel.client;
|
|
1505
|
+
const args = ['-R', routeSpec, destination];
|
|
1506
|
+
|
|
1507
|
+
if (tunnel.serverPort && tunnel.serverPort !== DEFAULT_TUNNEL_SERVER_PORT) {
|
|
1508
|
+
args.push('-p', `${tunnel.serverPort}`);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
if (tunnel.client === 'nssh') {
|
|
1512
|
+
args.push('--passwd', tunnel.password);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
return {
|
|
1516
|
+
command,
|
|
1517
|
+
args,
|
|
1518
|
+
routeSpec,
|
|
1519
|
+
destination,
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function resolveServicePaths() {
|
|
1524
|
+
const baseDir = path.join(os.homedir(), '.openclaw', 'mateclaw-openclaw-plugin');
|
|
1525
|
+
return {
|
|
1526
|
+
baseDir,
|
|
1527
|
+
pidFilePath: path.join(baseDir, 'connector-service.json'),
|
|
1528
|
+
outLogPath: path.join(baseDir, 'connector-service.out.log'),
|
|
1529
|
+
errLogPath: path.join(baseDir, 'connector-service.err.log'),
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function readServiceRecord() {
|
|
1534
|
+
const { pidFilePath } = resolveServicePaths();
|
|
1535
|
+
const snapshot = readJsonFileSafe(pidFilePath);
|
|
1536
|
+
if (!snapshot.exists || !snapshot.valid || !snapshot.value || typeof snapshot.value !== 'object') {
|
|
1537
|
+
return null;
|
|
1538
|
+
}
|
|
1539
|
+
return snapshot.value;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function isPidRunning(pid) {
|
|
1543
|
+
const numericPid = Number.parseInt(`${pid || ''}`, 10);
|
|
1544
|
+
if (!Number.isFinite(numericPid) || numericPid <= 0) {
|
|
1545
|
+
return false;
|
|
1546
|
+
}
|
|
1547
|
+
try {
|
|
1548
|
+
process.kill(numericPid, 0);
|
|
1549
|
+
return true;
|
|
1550
|
+
} catch {
|
|
1551
|
+
return false;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
function writeServiceRecord(record) {
|
|
1556
|
+
const { baseDir, pidFilePath } = resolveServicePaths();
|
|
1557
|
+
fs.mkdirSync(baseDir, { recursive: true });
|
|
1558
|
+
writeJsonFile(pidFilePath, record);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
function deleteServiceRecord() {
|
|
1562
|
+
const { pidFilePath } = resolveServicePaths();
|
|
1563
|
+
if (fs.existsSync(pidFilePath)) {
|
|
1564
|
+
fs.unlinkSync(pidFilePath);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function launchExternalTunnel(plan) {
|
|
1569
|
+
const commandText = `${plan.command} ${plan.args.join(' ')}`;
|
|
1570
|
+
try {
|
|
1571
|
+
const child = spawn(plan.command, plan.args, {
|
|
1572
|
+
shell: process.platform === 'win32',
|
|
1573
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
child.stdout?.on('data', (chunk) => {
|
|
1577
|
+
const text = `${chunk || ''}`.trim();
|
|
1578
|
+
if (text) {
|
|
1579
|
+
console.log(`[tunnel] ${text}`);
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
child.stderr?.on('data', (chunk) => {
|
|
1583
|
+
const text = `${chunk || ''}`.trim();
|
|
1584
|
+
if (text) {
|
|
1585
|
+
console.log(`[tunnel][stderr] ${text}`);
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
child.on('exit', (code, signal) => {
|
|
1589
|
+
const reason = signal ? `signal ${signal}` : `code ${code ?? 'unknown'}`;
|
|
1590
|
+
console.log(`[WARN] Tunnel process exited (${reason}).`);
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
return {
|
|
1594
|
+
enabled: true,
|
|
1595
|
+
started: true,
|
|
1596
|
+
child,
|
|
1597
|
+
stop: null,
|
|
1598
|
+
error: '',
|
|
1599
|
+
commandText,
|
|
1600
|
+
destination: plan.destination,
|
|
1601
|
+
routeSpec: plan.routeSpec,
|
|
1602
|
+
};
|
|
1603
|
+
} catch (error) {
|
|
1604
|
+
return {
|
|
1605
|
+
enabled: true,
|
|
1606
|
+
started: false,
|
|
1607
|
+
child: null,
|
|
1608
|
+
stop: null,
|
|
1609
|
+
error: `${error?.message || error}`,
|
|
1610
|
+
commandText,
|
|
1611
|
+
destination: plan.destination,
|
|
1612
|
+
routeSpec: plan.routeSpec,
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
async function launchBuiltinTunnel(config, plan) {
|
|
1618
|
+
const tunnel = config.tunnel;
|
|
1619
|
+
const commandText = `builtin-ssh -R ${plan.routeSpec} ${plan.destination}${
|
|
1620
|
+
tunnel.serverPort !== DEFAULT_TUNNEL_SERVER_PORT ? ` -p ${tunnel.serverPort}` : ''
|
|
1621
|
+
}`;
|
|
1622
|
+
|
|
1623
|
+
return await new Promise((resolve) => {
|
|
1624
|
+
const conn = new SshClient();
|
|
1625
|
+
let settled = false;
|
|
1626
|
+
|
|
1627
|
+
const settle = (runtime) => {
|
|
1628
|
+
if (settled) return;
|
|
1629
|
+
settled = true;
|
|
1630
|
+
clearTimeout(timer);
|
|
1631
|
+
resolve(runtime);
|
|
1632
|
+
};
|
|
1633
|
+
|
|
1634
|
+
const fail = (error) => {
|
|
1635
|
+
const detail = `${error?.message || error || 'unknown error'}`;
|
|
1636
|
+
try {
|
|
1637
|
+
conn.end();
|
|
1638
|
+
} catch {}
|
|
1639
|
+
settle({
|
|
1640
|
+
enabled: true,
|
|
1641
|
+
started: false,
|
|
1642
|
+
child: null,
|
|
1643
|
+
stop: null,
|
|
1644
|
+
error: detail,
|
|
1645
|
+
commandText,
|
|
1646
|
+
destination: plan.destination,
|
|
1647
|
+
routeSpec: plan.routeSpec,
|
|
1648
|
+
});
|
|
1649
|
+
};
|
|
1650
|
+
|
|
1651
|
+
const timer = setTimeout(() => {
|
|
1652
|
+
fail(new Error('builtin tunnel connection timeout'));
|
|
1653
|
+
}, 12000);
|
|
1654
|
+
|
|
1655
|
+
conn.on('ready', () => {
|
|
1656
|
+
conn.forwardIn('0.0.0.0', tunnel.remotePort, (error) => {
|
|
1657
|
+
if (error) {
|
|
1658
|
+
fail(error);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
settle({
|
|
1663
|
+
enabled: true,
|
|
1664
|
+
started: true,
|
|
1665
|
+
child: null,
|
|
1666
|
+
stop: () => {
|
|
1667
|
+
try {
|
|
1668
|
+
conn.end();
|
|
1669
|
+
} catch {}
|
|
1670
|
+
},
|
|
1671
|
+
error: '',
|
|
1672
|
+
commandText,
|
|
1673
|
+
destination: plan.destination,
|
|
1674
|
+
routeSpec: plan.routeSpec,
|
|
1675
|
+
});
|
|
1676
|
+
});
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
conn.on('tcp connection', (_details, accept) => {
|
|
1680
|
+
const remoteStream = accept();
|
|
1681
|
+
const localSocket = net.connect(tunnel.localPort, tunnel.localHost);
|
|
1682
|
+
|
|
1683
|
+
localSocket.on('error', (error) => {
|
|
1684
|
+
console.log(`[WARN] Tunnel local socket error: ${error?.message || error}`);
|
|
1685
|
+
try {
|
|
1686
|
+
remoteStream.destroy(error);
|
|
1687
|
+
} catch {}
|
|
1688
|
+
});
|
|
1689
|
+
remoteStream.on('error', (error) => {
|
|
1690
|
+
console.log(`[WARN] Tunnel remote stream error: ${error?.message || error}`);
|
|
1691
|
+
try {
|
|
1692
|
+
localSocket.destroy(error);
|
|
1693
|
+
} catch {}
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1696
|
+
remoteStream.pipe(localSocket);
|
|
1697
|
+
localSocket.pipe(remoteStream);
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
conn.on('error', (error) => {
|
|
1701
|
+
if (!settled) {
|
|
1702
|
+
fail(error);
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
console.log(`[WARN] Tunnel runtime error: ${error?.message || error}`);
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
conn.on('close', () => {
|
|
1709
|
+
if (settled) {
|
|
1710
|
+
console.log('[WARN] Tunnel process exited (builtin ssh closed).');
|
|
1711
|
+
}
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
conn.connect({
|
|
1715
|
+
host: tunnel.host,
|
|
1716
|
+
port: tunnel.serverPort,
|
|
1717
|
+
username: tunnel.user,
|
|
1718
|
+
password: tunnel.password,
|
|
1719
|
+
readyTimeout: 12000,
|
|
1720
|
+
keepaliveInterval: 15000,
|
|
1721
|
+
keepaliveCountMax: 4,
|
|
1722
|
+
});
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
async function launchTunnel(config) {
|
|
1727
|
+
const plan = buildTunnelLaunchPlan(config);
|
|
1728
|
+
if (!plan) {
|
|
1729
|
+
return {
|
|
1730
|
+
enabled: false,
|
|
1731
|
+
started: false,
|
|
1732
|
+
child: null,
|
|
1733
|
+
stop: null,
|
|
1734
|
+
error: '',
|
|
1735
|
+
commandText: '',
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
if (config.tunnel.client === 'builtin') {
|
|
1740
|
+
return await launchBuiltinTunnel(config, plan);
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
return launchExternalTunnel(plan);
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1191
1746
|
async function startConnector(config) {
|
|
1192
1747
|
const server = http.createServer((req, res) => {
|
|
1193
1748
|
handleRequest(req, res, config).catch((error) => {
|
|
@@ -1203,11 +1758,22 @@ async function startConnector(config) {
|
|
|
1203
1758
|
server.listen(config.port, config.listenHost, resolve);
|
|
1204
1759
|
});
|
|
1205
1760
|
|
|
1761
|
+
const tunnelRuntime = await launchTunnel(config);
|
|
1762
|
+
config.tunnelRuntime = tunnelRuntime;
|
|
1763
|
+
|
|
1206
1764
|
printBanner(config);
|
|
1207
1765
|
|
|
1208
1766
|
const shutdown = () => {
|
|
1209
|
-
console.log('\nShutting down
|
|
1210
|
-
server.close(() => process.exit(0));
|
|
1767
|
+
console.log('\nShutting down MateClaw OpenClaw Connector...');
|
|
1768
|
+
const done = () => server.close(() => process.exit(0));
|
|
1769
|
+
if (typeof tunnelRuntime?.stop === 'function') {
|
|
1770
|
+
console.log('Stopping tunnel process...');
|
|
1771
|
+
tunnelRuntime.stop();
|
|
1772
|
+
} else if (tunnelRuntime?.child && !tunnelRuntime.child.killed) {
|
|
1773
|
+
console.log('Stopping tunnel process...');
|
|
1774
|
+
tunnelRuntime.child.kill();
|
|
1775
|
+
}
|
|
1776
|
+
done();
|
|
1211
1777
|
};
|
|
1212
1778
|
|
|
1213
1779
|
process.on('SIGINT', shutdown);
|
|
@@ -1227,7 +1793,7 @@ async function handleRequest(req, res, config) {
|
|
|
1227
1793
|
if (requestUrl.pathname === '/health' && req.method === 'GET') {
|
|
1228
1794
|
sendJson(res, 200, {
|
|
1229
1795
|
ok: true,
|
|
1230
|
-
mode:
|
|
1796
|
+
mode: PAYLOAD_TYPE,
|
|
1231
1797
|
chatMode: config.chatMode,
|
|
1232
1798
|
sessionId: config.sessionKey,
|
|
1233
1799
|
upstream: config.openclawUrl,
|
|
@@ -2058,6 +2624,30 @@ function stripTrailingSlash(value) {
|
|
|
2058
2624
|
return value.replace(/\/+$/, '');
|
|
2059
2625
|
}
|
|
2060
2626
|
|
|
2627
|
+
function normalizePublicBaseUrl(value) {
|
|
2628
|
+
const raw = `${value || ''}`.trim();
|
|
2629
|
+
if (!raw) return '';
|
|
2630
|
+
const withScheme = /^https?:\/\//i.test(raw) ? raw : `http://${raw}`;
|
|
2631
|
+
try {
|
|
2632
|
+
const parsed = new URL(withScheme);
|
|
2633
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
2634
|
+
throw new Error('Public base URL must start with http:// or https://');
|
|
2635
|
+
}
|
|
2636
|
+
return stripTrailingSlash(parsed.toString());
|
|
2637
|
+
} catch {
|
|
2638
|
+
throw new Error(`Invalid public-base-url: ${value}`);
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
function normalizeTunnelClient(value) {
|
|
2643
|
+
const normalized = `${value || ''}`.trim().toLowerCase();
|
|
2644
|
+
if (!normalized) return DEFAULT_TUNNEL_CLIENT;
|
|
2645
|
+
if (normalized === 'builtin' || normalized === 'ssh' || normalized === 'nssh') {
|
|
2646
|
+
return normalized;
|
|
2647
|
+
}
|
|
2648
|
+
throw new Error(`Invalid tunnel-client: ${value}. Use builtin, ssh or nssh.`);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2061
2651
|
function normalizePath(value) {
|
|
2062
2652
|
if (!value) {
|
|
2063
2653
|
return DEFAULT_CHAT_PATH;
|
|
@@ -2235,8 +2825,9 @@ function isSpecialUseIpv4(address) {
|
|
|
2235
2825
|
}
|
|
2236
2826
|
|
|
2237
2827
|
function printBanner(config) {
|
|
2828
|
+
const tunnelRuntime = config.tunnelRuntime || {};
|
|
2238
2829
|
console.log('');
|
|
2239
|
-
console.log('MateClaw
|
|
2830
|
+
console.log('MateClaw OpenClaw Connector');
|
|
2240
2831
|
console.log('======================================');
|
|
2241
2832
|
console.log(`Upstream OpenClaw : ${config.openclawUrl}`);
|
|
2242
2833
|
console.log(`Chat Mode : ${config.chatMode}`);
|
|
@@ -2244,9 +2835,10 @@ function printBanner(config) {
|
|
|
2244
2835
|
console.log(`Chat Path : ${config.chatPath}`);
|
|
2245
2836
|
console.log(`Accepted Paths : ${config.acceptedChatPaths.join(', ')}`);
|
|
2246
2837
|
console.log(`Desktop Bind Host : ${config.listenHost}:${config.port}`);
|
|
2838
|
+
console.log(`Local Access URL : ${config.localBaseUrl}`);
|
|
2247
2839
|
console.log(`Mobile Access URL : ${config.baseUrl}`);
|
|
2248
2840
|
console.log(`Agent ID : ${config.agentId}`);
|
|
2249
|
-
console.log(`Token : ${config.token}`);
|
|
2841
|
+
console.log(`Token : ${maskToken(config.token)}`);
|
|
2250
2842
|
console.log(`Expires At : ${config.expiresAt.toISOString()}`);
|
|
2251
2843
|
console.log(`Upstream Timeout : ${config.upstreamTimeoutMs}ms`);
|
|
2252
2844
|
console.log(
|
|
@@ -2260,13 +2852,36 @@ function printBanner(config) {
|
|
|
2260
2852
|
console.log(` - ${item.address} (${item.iface}, score=${item.score})`);
|
|
2261
2853
|
}
|
|
2262
2854
|
}
|
|
2855
|
+
if (config.tunnel?.enabled) {
|
|
2856
|
+
console.log('Tunnel : enabled');
|
|
2857
|
+
console.log(`Tunnel Client : ${config.tunnel.client}`);
|
|
2858
|
+
if (tunnelRuntime.started) {
|
|
2859
|
+
console.log(
|
|
2860
|
+
`Tunnel Route : ${config.tunnel.remotePort} -> ${config.tunnel.localHost}:${config.tunnel.localPort} (${config.tunnel.user}@${config.tunnel.host})`,
|
|
2861
|
+
);
|
|
2862
|
+
console.log(`Tunnel Command : ${maskTunnelPasswordInCommand(tunnelRuntime.commandText)}`);
|
|
2863
|
+
} else {
|
|
2864
|
+
console.log(`Tunnel Status : failed to start (${tunnelRuntime.error || 'unknown error'})`);
|
|
2865
|
+
if (tunnelRuntime.commandText) {
|
|
2866
|
+
console.log(`Tunnel Command : ${maskTunnelPasswordInCommand(tunnelRuntime.commandText)}`);
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
if (!config.publicBaseUrl) {
|
|
2870
|
+
console.log('[WARN] Tunnel is enabled but --public-base-url is missing. QR still points to LAN URL.');
|
|
2871
|
+
}
|
|
2872
|
+
} else if (config.lanOnly) {
|
|
2873
|
+
console.log('Tunnel : disabled (LAN-only mode)');
|
|
2874
|
+
}
|
|
2263
2875
|
console.log('');
|
|
2264
|
-
console.log('
|
|
2876
|
+
console.log('One-command bind flow: install once -> scan the QR shown here -> chat.');
|
|
2265
2877
|
console.log('If binding fails, pin the LAN IP manually:');
|
|
2266
2878
|
console.log(
|
|
2267
2879
|
` node ./src/cli.mjs install --lan-host ${config.lanHost || '<your-lan-ip>'} --port ${config.port}`,
|
|
2268
2880
|
);
|
|
2269
|
-
console.log(`Quick health URL: ${config.
|
|
2881
|
+
console.log(`Quick health URL (local): ${config.localBaseUrl}/health`);
|
|
2882
|
+
if (config.publicBaseUrl) {
|
|
2883
|
+
console.log(`Public bind URL: ${config.publicBaseUrl}`);
|
|
2884
|
+
}
|
|
2270
2885
|
console.log('');
|
|
2271
2886
|
console.log('Scan this QR code in the MateClaw app (this is the OpenClaw-side bind QR):');
|
|
2272
2887
|
console.log('');
|