openclaw-cascade-plugin 1.0.12 → 1.0.14
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/grpc-client.d.ts +17 -0
- package/dist/grpc-client.d.ts.map +1 -0
- package/dist/grpc-client.js +154 -0
- package/dist/grpc-client.js.map +1 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -54
- package/dist/index.js.map +1 -1
- package/dist/test-utils/mocks.d.ts +6 -4
- package/dist/test-utils/mocks.d.ts.map +1 -1
- package/dist/test-utils/mocks.js +24 -14
- package/dist/test-utils/mocks.js.map +1 -1
- package/dist/tools/desktop-automation.d.ts +2 -7
- package/dist/tools/desktop-automation.d.ts.map +1 -1
- package/dist/tools/desktop-automation.js +64 -123
- package/dist/tools/desktop-automation.js.map +1 -1
- package/dist/tools/index.d.ts +3 -10
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -17
- package/dist/tools/index.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +13 -2
- package/proto/cascade.proto +297 -0
- package/PHASE1_SUMMARY.md +0 -191
- package/PHASE3_SUMMARY.md +0 -195
- package/dist/cascade-client.d.ts +0 -53
- package/dist/cascade-client.d.ts.map +0 -1
- package/dist/cascade-client.js +0 -179
- package/dist/cascade-client.js.map +0 -1
- package/dist/python-manager.d.ts +0 -59
- package/dist/python-manager.d.ts.map +0 -1
- package/dist/python-manager.js +0 -190
- package/dist/python-manager.js.map +0 -1
- package/dist/tools/api-tools.d.ts +0 -9
- package/dist/tools/api-tools.d.ts.map +0 -1
- package/dist/tools/api-tools.js +0 -102
- package/dist/tools/api-tools.js.map +0 -1
- package/dist/tools/sandbox-tools.d.ts +0 -9
- package/dist/tools/sandbox-tools.d.ts.map +0 -1
- package/dist/tools/sandbox-tools.js +0 -79
- package/dist/tools/sandbox-tools.js.map +0 -1
- package/dist/tools/web-automation.d.ts +0 -9
- package/dist/tools/web-automation.d.ts.map +0 -1
- package/dist/tools/web-automation.js +0 -471
- package/dist/tools/web-automation.js.map +0 -1
- package/jest.setup.js +0 -19
- package/openclaw-cascade-plugin-1.0.0.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.10.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.11.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.12.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.4.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.6.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.7.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.8.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.9.tgz +0 -0
- package/scripts/postinstall.js +0 -84
- package/src/a2a-client.ts +0 -66
- package/src/cascade-client.test.ts +0 -400
- package/src/cascade-client.ts +0 -198
- package/src/config.test.ts +0 -189
- package/src/config.ts +0 -137
- package/src/index.ts +0 -202
- package/src/python-manager.test.ts +0 -187
- package/src/python-manager.ts +0 -230
- package/src/test-utils/helpers.ts +0 -107
- package/src/test-utils/index.ts +0 -2
- package/src/test-utils/mocks.ts +0 -101
- package/src/tools/a2a-tools.ts +0 -162
- package/src/tools/api-tools.ts +0 -110
- package/src/tools/desktop-automation.test.ts +0 -308
- package/src/tools/desktop-automation.ts +0 -366
- package/src/tools/index.ts +0 -13
- package/src/tools/response-helpers.ts +0 -78
- package/src/tools/sandbox-tools.ts +0 -83
- package/src/tools/tool-registry.ts +0 -51
- package/src/tools/web-automation.test.ts +0 -177
- package/src/tools/web-automation.ts +0 -518
- package/src/types/index.ts +0 -133
- package/src/wsl.ts +0 -53
- package/tsconfig.json +0 -27
package/src/cascade-client.ts
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cascade MCP Client
|
|
3
|
-
*
|
|
4
|
-
* Handles communication with Cascade MCP server via stdio JSON-RPC
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { spawn, ChildProcess } from 'child_process';
|
|
8
|
-
|
|
9
|
-
export class CascadeMcpClient {
|
|
10
|
-
private process: ChildProcess | null = null;
|
|
11
|
-
private messageId = 0;
|
|
12
|
-
private pendingRequests = new Map<number, { resolve: Function; reject: Function }>();
|
|
13
|
-
private isInitialized = false;
|
|
14
|
-
private readonly REQUEST_TIMEOUT = 30000; // 30 seconds
|
|
15
|
-
private buffer = '';
|
|
16
|
-
|
|
17
|
-
constructor(
|
|
18
|
-
private pythonPath: string,
|
|
19
|
-
private env: NodeJS.ProcessEnv
|
|
20
|
-
) {}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Start the MCP client and initialize connection
|
|
24
|
-
*/
|
|
25
|
-
async start(): Promise<void> {
|
|
26
|
-
try {
|
|
27
|
-
// Spawn Cascade MCP server
|
|
28
|
-
this.process = spawn(this.pythonPath, ['-m', 'mcp_server.cli'], {
|
|
29
|
-
env: { ...process.env, ...this.env },
|
|
30
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// Handle stdout for JSON-RPC responses
|
|
34
|
-
if (this.process.stdout) {
|
|
35
|
-
this.process.stdout.on('data', (data: Buffer) => {
|
|
36
|
-
this.handleData(data.toString());
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Handle stderr
|
|
41
|
-
if (this.process.stderr) {
|
|
42
|
-
this.process.stderr.on('data', (data: Buffer) => {
|
|
43
|
-
console.error('Cascade MCP stderr:', data.toString());
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Handle process errors
|
|
48
|
-
this.process.on('error', (error: Error) => {
|
|
49
|
-
console.error('Cascade MCP process error:', error);
|
|
50
|
-
this.rejectAllPending(error);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Initialize MCP connection
|
|
54
|
-
await this.sendRequest('initialize', {
|
|
55
|
-
protocolVersion: '2024-11-05',
|
|
56
|
-
capabilities: {},
|
|
57
|
-
clientInfo: { name: 'openclaw-cascade', version: '1.0.0' }
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
this.isInitialized = true;
|
|
61
|
-
} catch (error) {
|
|
62
|
-
this.stop();
|
|
63
|
-
throw error;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Call a tool by name with arguments
|
|
69
|
-
*/
|
|
70
|
-
async callTool(name: string, args: Record<string, any>): Promise<any> {
|
|
71
|
-
if (!this.isInitialized) {
|
|
72
|
-
throw new Error('MCP client not initialized. Call start() first.');
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return this.sendRequest('tools/call', { name, arguments: args });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* List all available tools
|
|
80
|
-
*/
|
|
81
|
-
async listTools(): Promise<any[]> {
|
|
82
|
-
if (!this.isInitialized) {
|
|
83
|
-
throw new Error('MCP client not initialized. Call start() first.');
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const result = await this.sendRequest('tools/list', {});
|
|
87
|
-
return result.tools || [];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Check if client is connected
|
|
92
|
-
*/
|
|
93
|
-
isConnected(): boolean {
|
|
94
|
-
return this.isInitialized && this.process !== null && !this.process.killed;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Stop the MCP client
|
|
99
|
-
*/
|
|
100
|
-
stop(): void {
|
|
101
|
-
this.rejectAllPending(new Error('Client stopped'));
|
|
102
|
-
|
|
103
|
-
if (this.process && !this.process.killed) {
|
|
104
|
-
this.process.kill();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
this.process = null;
|
|
108
|
-
this.isInitialized = false;
|
|
109
|
-
this.buffer = '';
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Handle incoming data from stdout
|
|
114
|
-
*/
|
|
115
|
-
private handleData(data: string): void {
|
|
116
|
-
this.buffer += data;
|
|
117
|
-
|
|
118
|
-
// Process complete lines
|
|
119
|
-
const lines = this.buffer.split('\n');
|
|
120
|
-
this.buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
121
|
-
|
|
122
|
-
for (const line of lines) {
|
|
123
|
-
if (line.trim()) {
|
|
124
|
-
this.handleResponse(line);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Send a JSON-RPC request
|
|
131
|
-
*/
|
|
132
|
-
private async sendRequest(method: string, params: any): Promise<any> {
|
|
133
|
-
const id = ++this.messageId;
|
|
134
|
-
const request = { jsonrpc: '2.0', id, method, params };
|
|
135
|
-
|
|
136
|
-
return new Promise((resolve, reject) => {
|
|
137
|
-
// Set timeout
|
|
138
|
-
const timeoutId = setTimeout(() => {
|
|
139
|
-
this.pendingRequests.delete(id);
|
|
140
|
-
reject(new Error(`Request ${method} timed out after ${this.REQUEST_TIMEOUT}ms`));
|
|
141
|
-
}, this.REQUEST_TIMEOUT);
|
|
142
|
-
|
|
143
|
-
// Store pending request
|
|
144
|
-
this.pendingRequests.set(id, {
|
|
145
|
-
resolve: (result: any) => {
|
|
146
|
-
clearTimeout(timeoutId);
|
|
147
|
-
resolve(result);
|
|
148
|
-
},
|
|
149
|
-
reject: (error: Error) => {
|
|
150
|
-
clearTimeout(timeoutId);
|
|
151
|
-
reject(error);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// Send request
|
|
156
|
-
if (this.process?.stdin) {
|
|
157
|
-
this.process.stdin.write(JSON.stringify(request) + '\n');
|
|
158
|
-
} else {
|
|
159
|
-
this.pendingRequests.delete(id);
|
|
160
|
-
clearTimeout(timeoutId);
|
|
161
|
-
reject(new Error('Process stdin not available'));
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Handle incoming JSON-RPC response
|
|
168
|
-
*/
|
|
169
|
-
private handleResponse(line: string): void {
|
|
170
|
-
try {
|
|
171
|
-
const response = JSON.parse(line);
|
|
172
|
-
|
|
173
|
-
if (response.id && this.pendingRequests.has(response.id)) {
|
|
174
|
-
const { resolve, reject } = this.pendingRequests.get(response.id)!;
|
|
175
|
-
this.pendingRequests.delete(response.id);
|
|
176
|
-
|
|
177
|
-
if (response.error) {
|
|
178
|
-
reject(new Error(response.error.message || 'Unknown error'));
|
|
179
|
-
} else {
|
|
180
|
-
resolve(response.result);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
} catch (e) {
|
|
184
|
-
// Ignore parse errors for non-JSON lines
|
|
185
|
-
console.debug('Failed to parse MCP response:', line.substring(0, 100));
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Reject all pending requests
|
|
191
|
-
*/
|
|
192
|
-
private rejectAllPending(error: Error): void {
|
|
193
|
-
for (const [_id, { reject }] of this.pendingRequests) {
|
|
194
|
-
reject(error);
|
|
195
|
-
}
|
|
196
|
-
this.pendingRequests.clear();
|
|
197
|
-
}
|
|
198
|
-
}
|
package/src/config.test.ts
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Config
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { loadConfig, validateConfig, getDefaults } from './config';
|
|
6
|
-
import { CascadePluginConfig } from './types';
|
|
7
|
-
|
|
8
|
-
describe('Config', () => {
|
|
9
|
-
const originalEnv = process.env;
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
process.env = { ...originalEnv };
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
process.env = originalEnv;
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe('getDefaults', () => {
|
|
20
|
-
test('should return default configuration', async () => {
|
|
21
|
-
// Act
|
|
22
|
-
const defaults = await getDefaults();
|
|
23
|
-
|
|
24
|
-
// Assert
|
|
25
|
-
expect(defaults).toEqual({
|
|
26
|
-
cascadeGrpcEndpoint: 'localhost:50051',
|
|
27
|
-
headless: false,
|
|
28
|
-
actionTimeoutMs: 8000,
|
|
29
|
-
enableA2A: true,
|
|
30
|
-
verbose: false,
|
|
31
|
-
screenshotMode: 'auto'
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
describe('validateConfig', () => {
|
|
37
|
-
test('should pass with valid config', async () => {
|
|
38
|
-
// Arrange
|
|
39
|
-
const config: CascadePluginConfig = {
|
|
40
|
-
cascadeGrpcEndpoint: 'localhost:50051'
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// Act & Assert
|
|
44
|
-
expect(() => validateConfig(config)).not.toThrow();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('should throw when cascadeGrpcEndpoint is missing', async () => {
|
|
48
|
-
// Arrange
|
|
49
|
-
const config: CascadePluginConfig = {} as any;
|
|
50
|
-
|
|
51
|
-
// Act & Assert
|
|
52
|
-
expect(() => validateConfig(config)).toThrow('cascadeGrpcEndpoint is required');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('should throw when cascadeGrpcEndpoint is empty', async () => {
|
|
56
|
-
// Arrange
|
|
57
|
-
const config: CascadePluginConfig = {
|
|
58
|
-
cascadeGrpcEndpoint: ''
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// Act & Assert
|
|
62
|
-
expect(() => validateConfig(config)).toThrow('cascadeGrpcEndpoint is required');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('should validate firestore credentials path if provided', async () => {
|
|
66
|
-
// Arrange
|
|
67
|
-
const config: CascadePluginConfig = {
|
|
68
|
-
cascadeGrpcEndpoint: 'localhost:50051',
|
|
69
|
-
firestoreCredentialsPath: '/path/to/creds.json'
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
// Act & Assert
|
|
73
|
-
expect(() => validateConfig(config)).not.toThrow();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('should validate allowed agents', async () => {
|
|
77
|
-
// Arrange
|
|
78
|
-
const config: CascadePluginConfig = {
|
|
79
|
-
cascadeGrpcEndpoint: 'localhost:50051',
|
|
80
|
-
allowedAgents: ['explorer', 'worker', 'orchestrator']
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// Act & Assert
|
|
84
|
-
expect(() => validateConfig(config)).not.toThrow();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test('should throw for invalid agent in allowedAgents', async () => {
|
|
88
|
-
// Arrange
|
|
89
|
-
const config: any = {
|
|
90
|
-
cascadeGrpcEndpoint: 'localhost:50051',
|
|
91
|
-
allowedAgents: ['explorer', 'invalid-agent']
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
// Act & Assert
|
|
95
|
-
expect(() => validateConfig(config)).toThrow('Invalid agent');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('should validate screenshotMode', async () => {
|
|
99
|
-
// Arrange
|
|
100
|
-
const config: CascadePluginConfig = {
|
|
101
|
-
cascadeGrpcEndpoint: 'localhost:50051',
|
|
102
|
-
screenshotMode: 'embed'
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
// Act & Assert
|
|
106
|
-
expect(() => validateConfig(config)).not.toThrow();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test('should throw for invalid screenshotMode', async () => {
|
|
110
|
-
// Arrange
|
|
111
|
-
const config: any = {
|
|
112
|
-
cascadeGrpcEndpoint: 'localhost:50051',
|
|
113
|
-
screenshotMode: 'invalid'
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
// Act & Assert
|
|
117
|
-
expect(() => validateConfig(config)).toThrow('Invalid screenshotMode');
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
describe('loadConfig', () => {
|
|
122
|
-
test('should load config with defaults when input is empty', async () => {
|
|
123
|
-
// Act
|
|
124
|
-
const config = await loadConfig({});
|
|
125
|
-
|
|
126
|
-
// Assert
|
|
127
|
-
expect(config.cascadeGrpcEndpoint).toBe('localhost:50051');
|
|
128
|
-
expect(config.headless).toBe(false);
|
|
129
|
-
expect(config.actionTimeoutMs).toBe(8000);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('should merge with provided values', async () => {
|
|
133
|
-
// Arrange
|
|
134
|
-
const input = {
|
|
135
|
-
cascadeGrpcEndpoint: '192.168.1.100:50051',
|
|
136
|
-
headless: true,
|
|
137
|
-
actionTimeoutMs: 15000
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
// Act
|
|
141
|
-
const config = await loadConfig(input);
|
|
142
|
-
|
|
143
|
-
// Assert
|
|
144
|
-
expect(config.cascadeGrpcEndpoint).toBe('192.168.1.100:50051');
|
|
145
|
-
expect(config.headless).toBe(true);
|
|
146
|
-
expect(config.actionTimeoutMs).toBe(15000);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test('should expand environment variables in paths', async () => {
|
|
150
|
-
// Arrange
|
|
151
|
-
process.env.HOME = '/home/user';
|
|
152
|
-
const input = {
|
|
153
|
-
cascadeGrpcEndpoint: 'localhost:50051',
|
|
154
|
-
firestoreCredentialsPath: '$HOME/creds.json'
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
// Act
|
|
158
|
-
const config = await loadConfig(input);
|
|
159
|
-
|
|
160
|
-
// Assert
|
|
161
|
-
expect(config.firestoreCredentialsPath).toBe('/home/user/creds.json');
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test('should handle Windows environment variables', async () => {
|
|
165
|
-
// Arrange
|
|
166
|
-
process.env.USERPROFILE = 'C:\\Users\\TestUser';
|
|
167
|
-
const input = {
|
|
168
|
-
cascadeGrpcEndpoint: 'localhost:50051',
|
|
169
|
-
cascadePythonPath: '%USERPROFILE%\\Python\\python.exe'
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
// Act
|
|
173
|
-
const config = await loadConfig(input);
|
|
174
|
-
|
|
175
|
-
// Assert
|
|
176
|
-
expect(config.cascadePythonPath).toBe('C:\\Users\\TestUser\\Python\\python.exe');
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
test('should validate loaded config', async () => {
|
|
180
|
-
// Arrange
|
|
181
|
-
const input = {
|
|
182
|
-
cascadeGrpcEndpoint: ''
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
// Act & Assert
|
|
186
|
-
await expect(loadConfig(input as any)).rejects.toThrow('cascadeGrpcEndpoint is required');
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
});
|
package/src/config.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configuration management for OpenClaw Cascade Plugin
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { CascadePluginConfig } from './types';
|
|
6
|
-
import { isWsl, getWslHostIp } from './wsl';
|
|
7
|
-
|
|
8
|
-
const VALID_AGENTS = ['explorer', 'worker', 'orchestrator'];
|
|
9
|
-
const VALID_SCREENSHOT_MODES = ['embed', 'disk', 'auto'];
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Get default configuration values
|
|
13
|
-
*/
|
|
14
|
-
export async function getDefaults(): Promise<Partial<CascadePluginConfig>> {
|
|
15
|
-
let defaultEndpoint = 'localhost:50051';
|
|
16
|
-
|
|
17
|
-
if (isWsl()) {
|
|
18
|
-
const wslIp = await getWslHostIp();
|
|
19
|
-
if (wslIp) {
|
|
20
|
-
defaultEndpoint = `${wslIp}:50051`;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
cascadeGrpcEndpoint: defaultEndpoint,
|
|
26
|
-
headless: false,
|
|
27
|
-
actionTimeoutMs: 8000,
|
|
28
|
-
enableA2A: true,
|
|
29
|
-
verbose: false,
|
|
30
|
-
screenshotMode: 'auto'
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Validate configuration object
|
|
36
|
-
*/
|
|
37
|
-
export function validateConfig(config: CascadePluginConfig): void {
|
|
38
|
-
// Required fields
|
|
39
|
-
if (!config.cascadeGrpcEndpoint || config.cascadeGrpcEndpoint.trim() === '') {
|
|
40
|
-
throw new Error('cascadeGrpcEndpoint is required');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Validate gRPC endpoint format
|
|
44
|
-
const endpointPattern = /^[\w.-]+:\d+$/;
|
|
45
|
-
if (!endpointPattern.test(config.cascadeGrpcEndpoint)) {
|
|
46
|
-
throw new Error(
|
|
47
|
-
`Invalid cascadeGrpcEndpoint format: ${config.cascadeGrpcEndpoint}. ` +
|
|
48
|
-
'Expected format: host:port (e.g., localhost:50051)'
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Validate allowed agents if provided
|
|
53
|
-
if (config.allowedAgents) {
|
|
54
|
-
for (const agent of config.allowedAgents) {
|
|
55
|
-
if (!VALID_AGENTS.includes(agent)) {
|
|
56
|
-
throw new Error(`Invalid agent in allowedAgents: ${agent}. Valid agents: ${VALID_AGENTS.join(', ')}`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Validate screenshot mode if provided
|
|
62
|
-
if (config.screenshotMode && !VALID_SCREENSHOT_MODES.includes(config.screenshotMode)) {
|
|
63
|
-
throw new Error(
|
|
64
|
-
`Invalid screenshotMode: ${config.screenshotMode}. ` +
|
|
65
|
-
`Valid modes: ${VALID_SCREENSHOT_MODES.join(', ')}`
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Validate action timeout
|
|
70
|
-
if (config.actionTimeoutMs !== undefined && config.actionTimeoutMs < 1000) {
|
|
71
|
-
throw new Error('actionTimeoutMs must be at least 1000ms');
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Expand environment variables in a string
|
|
77
|
-
* Supports both $VAR and %VAR% syntax
|
|
78
|
-
*/
|
|
79
|
-
export function expandEnvVars(value: string): string {
|
|
80
|
-
if (!value) return value;
|
|
81
|
-
|
|
82
|
-
// Unix-style: $VAR or ${VAR}
|
|
83
|
-
let expanded = value.replace(/\$\{(\w+)\}/g, (match, varName) => {
|
|
84
|
-
return process.env[varName] || match;
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
expanded = expanded.replace(/\$(\w+)/g, (match, varName) => {
|
|
88
|
-
return process.env[varName] || match;
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// Windows-style: %VAR%
|
|
92
|
-
expanded = expanded.replace(/%(\w+)%/g, (match, varName) => {
|
|
93
|
-
return process.env[varName] || match;
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
return expanded;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Load and validate configuration
|
|
101
|
-
*/
|
|
102
|
-
export async function loadConfig(input?: Partial<CascadePluginConfig>): Promise<CascadePluginConfig> {
|
|
103
|
-
const actualInput = input || {};
|
|
104
|
-
|
|
105
|
-
// Expand environment variables in string fields
|
|
106
|
-
const expanded: Partial<CascadePluginConfig> = {};
|
|
107
|
-
|
|
108
|
-
for (const [key, value] of Object.entries(actualInput)) {
|
|
109
|
-
if (typeof value === 'string') {
|
|
110
|
-
(expanded as any)[key] = expandEnvVars(value);
|
|
111
|
-
} else {
|
|
112
|
-
(expanded as any)[key] = value;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Merge with defaults
|
|
117
|
-
const defaults = await getDefaults();
|
|
118
|
-
const config: CascadePluginConfig = {
|
|
119
|
-
...defaults,
|
|
120
|
-
...expanded
|
|
121
|
-
} as CascadePluginConfig;
|
|
122
|
-
|
|
123
|
-
// Validate
|
|
124
|
-
validateConfig(config);
|
|
125
|
-
|
|
126
|
-
return config;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Load configuration from OpenClaw API
|
|
131
|
-
*/
|
|
132
|
-
export async function loadConfigFromOpenClaw(api: any): Promise<CascadePluginConfig> {
|
|
133
|
-
const entries = api.config?.plugins?.entries || {};
|
|
134
|
-
const pluginConfig = entries['openclaw-cascade-plugin']?.config || entries.cascade?.config || {};
|
|
135
|
-
|
|
136
|
-
return loadConfig(pluginConfig);
|
|
137
|
-
}
|