echoclaw-relay-agent 0.19.1 → 0.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/local/LocalAgent.d.ts +51 -0
- package/dist/local/LocalAgent.js +200 -0
- package/dist/local/LocalDetector.d.ts +19 -0
- package/dist/local/LocalDetector.js +77 -0
- package/dist/local/index.d.ts +5 -0
- package/dist/local/index.js +5 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -27,3 +27,5 @@ export { GatewayWatchdog } from './gateway/index.js';
|
|
|
27
27
|
export { OpenClawWsClient } from './gateway/index.js';
|
|
28
28
|
export type { GatewayConfig, BridgeEndpoint, BridgeStatus, DeviceInfo, OpenClawWsClientConfig, } from './gateway/index.js';
|
|
29
29
|
export { DEFAULT_GATEWAY_CONFIG } from './gateway/index.js';
|
|
30
|
+
export { LocalAgent, type LocalAgentConfig, type LocalAgentStatus } from './local/index.js';
|
|
31
|
+
export { detectLocalOpenClaw, type LocalOpenClawInfo } from './local/index.js';
|
package/dist/index.js
CHANGED
|
@@ -30,3 +30,6 @@ export { GatewayBridge } from './gateway/index.js';
|
|
|
30
30
|
export { GatewayWatchdog } from './gateway/index.js';
|
|
31
31
|
export { OpenClawWsClient } from './gateway/index.js';
|
|
32
32
|
export { DEFAULT_GATEWAY_CONFIG } from './gateway/index.js';
|
|
33
|
+
// ── Local Connection ────────────────────────────────────────
|
|
34
|
+
export { LocalAgent } from './local/index.js';
|
|
35
|
+
export { detectLocalOpenClaw } from './local/index.js';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalAgent — Direct local connection to OpenClaw Gateway.
|
|
3
|
+
*
|
|
4
|
+
* Used when desktop and OpenClaw are on the same machine.
|
|
5
|
+
* Provides the SAME interface as RelayAgent — desktop doesn't know the difference.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* Desktop ← EventEmitter → LocalAgent → ChatHandler/InstallHandler → OpenClaw Gateway (localhost)
|
|
9
|
+
*
|
|
10
|
+
* No relay server, no encryption (localhost), no pairing needed.
|
|
11
|
+
* Reuses ChatHandler and InstallHandler via setSendBack() injection.
|
|
12
|
+
*/
|
|
13
|
+
import { EventEmitter } from 'node:events';
|
|
14
|
+
export interface LocalAgentConfig {
|
|
15
|
+
/** OpenClaw Gateway port. Default: 18789 */
|
|
16
|
+
gatewayPort?: number;
|
|
17
|
+
/** OpenClaw Gateway host. Default: '127.0.0.1' */
|
|
18
|
+
gatewayHost?: string;
|
|
19
|
+
/** Gateway auth token (from ~/.openclaw/openclaw.json) */
|
|
20
|
+
gatewayToken: string;
|
|
21
|
+
/** Session key. Default: 'main' */
|
|
22
|
+
sessionKey?: string;
|
|
23
|
+
}
|
|
24
|
+
export type LocalAgentStatus = 'disconnected' | 'connecting' | 'connected';
|
|
25
|
+
export declare class LocalAgent extends EventEmitter {
|
|
26
|
+
private readonly config;
|
|
27
|
+
private chatHandler;
|
|
28
|
+
private installHandler;
|
|
29
|
+
private _status;
|
|
30
|
+
private _stopped;
|
|
31
|
+
private _reconnectTimer;
|
|
32
|
+
private _reconnectAttempt;
|
|
33
|
+
constructor(config: LocalAgentConfig);
|
|
34
|
+
get status(): LocalAgentStatus;
|
|
35
|
+
get connectionMode(): 'local';
|
|
36
|
+
/**
|
|
37
|
+
* Connect to local OpenClaw Gateway.
|
|
38
|
+
*/
|
|
39
|
+
connect(): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Send a message to OpenClaw (from desktop).
|
|
42
|
+
* Routes to appropriate handler based on message type.
|
|
43
|
+
*/
|
|
44
|
+
send(payload: any): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Gracefully disconnect.
|
|
47
|
+
*/
|
|
48
|
+
disconnect(): Promise<void>;
|
|
49
|
+
private setStatus;
|
|
50
|
+
private _scheduleReconnect;
|
|
51
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalAgent — Direct local connection to OpenClaw Gateway.
|
|
3
|
+
*
|
|
4
|
+
* Used when desktop and OpenClaw are on the same machine.
|
|
5
|
+
* Provides the SAME interface as RelayAgent — desktop doesn't know the difference.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* Desktop ← EventEmitter → LocalAgent → ChatHandler/InstallHandler → OpenClaw Gateway (localhost)
|
|
9
|
+
*
|
|
10
|
+
* No relay server, no encryption (localhost), no pairing needed.
|
|
11
|
+
* Reuses ChatHandler and InstallHandler via setSendBack() injection.
|
|
12
|
+
*/
|
|
13
|
+
import { EventEmitter } from 'node:events';
|
|
14
|
+
import { ChatHandler } from '../chat/ChatHandler.js';
|
|
15
|
+
import { InstallHandler } from '../install/InstallHandler.js';
|
|
16
|
+
export class LocalAgent extends EventEmitter {
|
|
17
|
+
constructor(config) {
|
|
18
|
+
super();
|
|
19
|
+
Object.defineProperty(this, "config", {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: void 0
|
|
24
|
+
});
|
|
25
|
+
Object.defineProperty(this, "chatHandler", {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
configurable: true,
|
|
28
|
+
writable: true,
|
|
29
|
+
value: null
|
|
30
|
+
});
|
|
31
|
+
Object.defineProperty(this, "installHandler", {
|
|
32
|
+
enumerable: true,
|
|
33
|
+
configurable: true,
|
|
34
|
+
writable: true,
|
|
35
|
+
value: null
|
|
36
|
+
});
|
|
37
|
+
Object.defineProperty(this, "_status", {
|
|
38
|
+
enumerable: true,
|
|
39
|
+
configurable: true,
|
|
40
|
+
writable: true,
|
|
41
|
+
value: 'disconnected'
|
|
42
|
+
});
|
|
43
|
+
Object.defineProperty(this, "_stopped", {
|
|
44
|
+
enumerable: true,
|
|
45
|
+
configurable: true,
|
|
46
|
+
writable: true,
|
|
47
|
+
value: false
|
|
48
|
+
});
|
|
49
|
+
Object.defineProperty(this, "_reconnectTimer", {
|
|
50
|
+
enumerable: true,
|
|
51
|
+
configurable: true,
|
|
52
|
+
writable: true,
|
|
53
|
+
value: null
|
|
54
|
+
});
|
|
55
|
+
Object.defineProperty(this, "_reconnectAttempt", {
|
|
56
|
+
enumerable: true,
|
|
57
|
+
configurable: true,
|
|
58
|
+
writable: true,
|
|
59
|
+
value: 0
|
|
60
|
+
});
|
|
61
|
+
this.config = config;
|
|
62
|
+
}
|
|
63
|
+
get status() {
|
|
64
|
+
return this._status;
|
|
65
|
+
}
|
|
66
|
+
get connectionMode() {
|
|
67
|
+
return 'local';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Connect to local OpenClaw Gateway.
|
|
71
|
+
*/
|
|
72
|
+
async connect() {
|
|
73
|
+
this._stopped = false;
|
|
74
|
+
this.setStatus('connecting');
|
|
75
|
+
try {
|
|
76
|
+
const port = this.config.gatewayPort ?? 18789;
|
|
77
|
+
const host = this.config.gatewayHost ?? '127.0.0.1';
|
|
78
|
+
const sessionKey = this.config.sessionKey ?? 'main';
|
|
79
|
+
// Create ChatHandler — it manages the WS connection to Gateway
|
|
80
|
+
this.chatHandler = new ChatHandler({
|
|
81
|
+
port,
|
|
82
|
+
host,
|
|
83
|
+
gatewayToken: this.config.gatewayToken,
|
|
84
|
+
sessionKey,
|
|
85
|
+
});
|
|
86
|
+
// Inject sendBack: messages go directly to desktop via EventEmitter
|
|
87
|
+
this.chatHandler.setSendBack(async (msg) => {
|
|
88
|
+
this.emit('message', msg);
|
|
89
|
+
});
|
|
90
|
+
// Create InstallHandler — needs wsClient + chatHandler
|
|
91
|
+
this.installHandler = new InstallHandler(this.chatHandler.client, this.chatHandler, { sessionKey });
|
|
92
|
+
this.installHandler.setSendBack(async (msg) => {
|
|
93
|
+
this.emit('message', msg);
|
|
94
|
+
});
|
|
95
|
+
// Listen for Gateway connection events
|
|
96
|
+
this.chatHandler.client.on('connected', () => {
|
|
97
|
+
this._reconnectAttempt = 0;
|
|
98
|
+
this.setStatus('connected');
|
|
99
|
+
this.emit('connected');
|
|
100
|
+
});
|
|
101
|
+
this.chatHandler.client.on('disconnected', () => {
|
|
102
|
+
if (this._status === 'connected') {
|
|
103
|
+
this.setStatus('disconnected');
|
|
104
|
+
this.emit('disconnected');
|
|
105
|
+
}
|
|
106
|
+
if (!this._stopped) {
|
|
107
|
+
this._scheduleReconnect();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
this.chatHandler.client.on('error', (err) => {
|
|
111
|
+
this.emit('error', err);
|
|
112
|
+
});
|
|
113
|
+
// Connect to Gateway
|
|
114
|
+
await this.chatHandler.connect();
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
this.setStatus('disconnected');
|
|
118
|
+
if (!this._stopped) {
|
|
119
|
+
this._scheduleReconnect();
|
|
120
|
+
}
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Send a message to OpenClaw (from desktop).
|
|
126
|
+
* Routes to appropriate handler based on message type.
|
|
127
|
+
*/
|
|
128
|
+
async send(payload) {
|
|
129
|
+
if (!payload?.type)
|
|
130
|
+
return;
|
|
131
|
+
const type = payload.type;
|
|
132
|
+
// Chat messages
|
|
133
|
+
if (type === 'chat' || type === 'system_prompt') {
|
|
134
|
+
if (this.chatHandler) {
|
|
135
|
+
await this.chatHandler.handle(payload, async (msg) => {
|
|
136
|
+
this.emit('message', msg);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Install messages
|
|
142
|
+
if (type === 'install_request') {
|
|
143
|
+
if (this.installHandler) {
|
|
144
|
+
await this.installHandler.handleInstallRequest(payload);
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (type === 'install_abort') {
|
|
149
|
+
if (this.installHandler) {
|
|
150
|
+
await this.installHandler.handleAbort(payload.requestId);
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Unknown message type — ignore (forward compat)
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Gracefully disconnect.
|
|
158
|
+
*/
|
|
159
|
+
async disconnect() {
|
|
160
|
+
this._stopped = true;
|
|
161
|
+
if (this._reconnectTimer) {
|
|
162
|
+
clearTimeout(this._reconnectTimer);
|
|
163
|
+
this._reconnectTimer = null;
|
|
164
|
+
}
|
|
165
|
+
if (this.chatHandler) {
|
|
166
|
+
this.chatHandler.clearSendBack();
|
|
167
|
+
this.chatHandler.disconnect();
|
|
168
|
+
this.chatHandler = null;
|
|
169
|
+
}
|
|
170
|
+
if (this.installHandler) {
|
|
171
|
+
this.installHandler.clearSendBack();
|
|
172
|
+
this.installHandler = null;
|
|
173
|
+
}
|
|
174
|
+
this.setStatus('disconnected');
|
|
175
|
+
}
|
|
176
|
+
// ── Private ─────────────────────────────────────────────
|
|
177
|
+
setStatus(status) {
|
|
178
|
+
if (this._status !== status) {
|
|
179
|
+
this._status = status;
|
|
180
|
+
this.emit('status', status);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
_scheduleReconnect() {
|
|
184
|
+
if (this._stopped || this._reconnectTimer)
|
|
185
|
+
return;
|
|
186
|
+
this._reconnectAttempt++;
|
|
187
|
+
const delay = Math.min(3000 * Math.pow(2, this._reconnectAttempt - 1), 60000);
|
|
188
|
+
this._reconnectTimer = setTimeout(async () => {
|
|
189
|
+
this._reconnectTimer = null;
|
|
190
|
+
if (this._stopped)
|
|
191
|
+
return;
|
|
192
|
+
try {
|
|
193
|
+
await this.connect();
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// connect() failed, reconnect scheduled by disconnected event
|
|
197
|
+
}
|
|
198
|
+
}, delay);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalDetector — Detect local OpenClaw installation.
|
|
3
|
+
*
|
|
4
|
+
* Checks two conditions (both must pass):
|
|
5
|
+
* 1. OpenClaw Gateway is reachable at localhost:<port>
|
|
6
|
+
* 2. Gateway auth token is readable from ~/.openclaw/openclaw.json
|
|
7
|
+
*
|
|
8
|
+
* If both pass → local mode. Otherwise → relay mode.
|
|
9
|
+
*/
|
|
10
|
+
export interface LocalOpenClawInfo {
|
|
11
|
+
gatewayPort: number;
|
|
12
|
+
gatewayToken: string;
|
|
13
|
+
configPath: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Detect if OpenClaw is installed and running locally.
|
|
17
|
+
* Returns config info if available, null otherwise.
|
|
18
|
+
*/
|
|
19
|
+
export declare function detectLocalOpenClaw(configPath?: string, port?: number): Promise<LocalOpenClawInfo | null>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalDetector — Detect local OpenClaw installation.
|
|
3
|
+
*
|
|
4
|
+
* Checks two conditions (both must pass):
|
|
5
|
+
* 1. OpenClaw Gateway is reachable at localhost:<port>
|
|
6
|
+
* 2. Gateway auth token is readable from ~/.openclaw/openclaw.json
|
|
7
|
+
*
|
|
8
|
+
* If both pass → local mode. Otherwise → relay mode.
|
|
9
|
+
*/
|
|
10
|
+
import { readFile } from 'node:fs/promises';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
const DEFAULT_GATEWAY_PORT = 18789;
|
|
14
|
+
const OPENCLAW_CONFIG_PATH = join(homedir(), '.openclaw', 'openclaw.json');
|
|
15
|
+
const DETECT_TIMEOUT_MS = 2000;
|
|
16
|
+
/**
|
|
17
|
+
* Detect if OpenClaw is installed and running locally.
|
|
18
|
+
* Returns config info if available, null otherwise.
|
|
19
|
+
*/
|
|
20
|
+
export async function detectLocalOpenClaw(configPath = OPENCLAW_CONFIG_PATH, port = DEFAULT_GATEWAY_PORT) {
|
|
21
|
+
try {
|
|
22
|
+
// Step 1: Read gateway token from config file
|
|
23
|
+
const token = await readGatewayToken(configPath);
|
|
24
|
+
if (!token)
|
|
25
|
+
return null;
|
|
26
|
+
// Step 2: Check if Gateway is reachable
|
|
27
|
+
const reachable = await checkGatewayHealth(port, token);
|
|
28
|
+
if (!reachable)
|
|
29
|
+
return null;
|
|
30
|
+
return {
|
|
31
|
+
gatewayPort: port,
|
|
32
|
+
gatewayToken: token,
|
|
33
|
+
configPath,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Read gateway auth token from openclaw.json.
|
|
42
|
+
* Returns null if file doesn't exist or token not found.
|
|
43
|
+
*/
|
|
44
|
+
async function readGatewayToken(configPath) {
|
|
45
|
+
try {
|
|
46
|
+
const raw = await readFile(configPath, 'utf-8');
|
|
47
|
+
const config = JSON.parse(raw);
|
|
48
|
+
// Token location: gateway.auth.token or gateway.token
|
|
49
|
+
const token = config?.gateway?.auth?.token ||
|
|
50
|
+
config?.gateway?.token ||
|
|
51
|
+
null;
|
|
52
|
+
if (!token || typeof token !== 'string')
|
|
53
|
+
return null;
|
|
54
|
+
return token;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Ping Gateway health endpoint to verify it's running.
|
|
62
|
+
*/
|
|
63
|
+
async function checkGatewayHealth(port, token) {
|
|
64
|
+
try {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timeout = setTimeout(() => controller.abort(), DETECT_TIMEOUT_MS);
|
|
67
|
+
const res = await fetch(`http://127.0.0.1:${port}/v1/models`, {
|
|
68
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
69
|
+
signal: controller.signal,
|
|
70
|
+
});
|
|
71
|
+
clearTimeout(timeout);
|
|
72
|
+
return res.ok;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
package/package.json
CHANGED