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 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
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Local connection module — public API.
3
+ */
4
+ export { LocalAgent, type LocalAgentConfig, type LocalAgentStatus } from './LocalAgent.js';
5
+ export { detectLocalOpenClaw, type LocalOpenClawInfo } from './LocalDetector.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Local connection module — public API.
3
+ */
4
+ export { LocalAgent } from './LocalAgent.js';
5
+ export { detectLocalOpenClaw } from './LocalDetector.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echoclaw-relay-agent",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
4
4
  "description": "EchoClaw Relay Connection — E2E encrypted relay transport, pairing, and session management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",