openclaw-cascade-plugin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PHASE1_SUMMARY.md +191 -0
- package/PHASE3_SUMMARY.md +195 -0
- package/README.md +43 -0
- package/dist/a2a-client.d.ts +17 -0
- package/dist/a2a-client.d.ts.map +1 -0
- package/dist/a2a-client.js +47 -0
- package/dist/a2a-client.js.map +1 -0
- package/dist/cascade-client.d.ts +53 -0
- package/dist/cascade-client.d.ts.map +1 -0
- package/dist/cascade-client.js +179 -0
- package/dist/cascade-client.js.map +1 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +116 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +136 -0
- package/dist/index.js.map +1 -0
- package/dist/python-manager.d.ts +59 -0
- package/dist/python-manager.d.ts.map +1 -0
- package/dist/python-manager.js +190 -0
- package/dist/python-manager.js.map +1 -0
- package/dist/test-utils/helpers.d.ts +20 -0
- package/dist/test-utils/helpers.d.ts.map +1 -0
- package/dist/test-utils/helpers.js +89 -0
- package/dist/test-utils/helpers.js.map +1 -0
- package/dist/test-utils/index.d.ts +3 -0
- package/dist/test-utils/index.d.ts.map +1 -0
- package/dist/test-utils/index.js +19 -0
- package/dist/test-utils/index.js.map +1 -0
- package/dist/test-utils/mocks.d.ts +51 -0
- package/dist/test-utils/mocks.d.ts.map +1 -0
- package/dist/test-utils/mocks.js +84 -0
- package/dist/test-utils/mocks.js.map +1 -0
- package/dist/tools/a2a-tools.d.ts +9 -0
- package/dist/tools/a2a-tools.d.ts.map +1 -0
- package/dist/tools/a2a-tools.js +147 -0
- package/dist/tools/a2a-tools.js.map +1 -0
- package/dist/tools/api-tools.d.ts +9 -0
- package/dist/tools/api-tools.d.ts.map +1 -0
- package/dist/tools/api-tools.js +102 -0
- package/dist/tools/api-tools.js.map +1 -0
- package/dist/tools/desktop-automation.d.ts +10 -0
- package/dist/tools/desktop-automation.d.ts.map +1 -0
- package/dist/tools/desktop-automation.js +330 -0
- package/dist/tools/desktop-automation.js.map +1 -0
- package/dist/tools/index.d.ts +12 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +35 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/response-helpers.d.ts +25 -0
- package/dist/tools/response-helpers.d.ts.map +1 -0
- package/dist/tools/response-helpers.js +71 -0
- package/dist/tools/response-helpers.js.map +1 -0
- package/dist/tools/sandbox-tools.d.ts +9 -0
- package/dist/tools/sandbox-tools.d.ts.map +1 -0
- package/dist/tools/sandbox-tools.js +79 -0
- package/dist/tools/sandbox-tools.js.map +1 -0
- package/dist/tools/tool-registry.d.ts +34 -0
- package/dist/tools/tool-registry.d.ts.map +1 -0
- package/dist/tools/tool-registry.js +50 -0
- package/dist/tools/tool-registry.js.map +1 -0
- package/dist/tools/web-automation.d.ts +9 -0
- package/dist/tools/web-automation.d.ts.map +1 -0
- package/dist/tools/web-automation.js +471 -0
- package/dist/tools/web-automation.js.map +1 -0
- package/dist/types/index.d.ts +111 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +38 -0
- package/dist/types/index.js.map +1 -0
- package/jest.setup.js +19 -0
- package/openclaw-cascade-plugin-1.0.0.tgz +0 -0
- package/openclaw.plugin.json +116 -0
- package/package.json +74 -0
- package/src/a2a-client.ts +66 -0
- package/src/cascade-client.test.ts +400 -0
- package/src/cascade-client.ts +198 -0
- package/src/config.test.ts +194 -0
- package/src/config.ts +135 -0
- package/src/index.ts +164 -0
- package/src/python-manager.test.ts +187 -0
- package/src/python-manager.ts +230 -0
- package/src/test-utils/helpers.ts +107 -0
- package/src/test-utils/index.ts +2 -0
- package/src/test-utils/mocks.ts +101 -0
- package/src/tools/a2a-tools.ts +162 -0
- package/src/tools/api-tools.ts +110 -0
- package/src/tools/desktop-automation.test.ts +305 -0
- package/src/tools/desktop-automation.ts +366 -0
- package/src/tools/index.ts +13 -0
- package/src/tools/response-helpers.ts +78 -0
- package/src/tools/sandbox-tools.ts +83 -0
- package/src/tools/tool-registry.ts +51 -0
- package/src/tools/web-automation.test.ts +177 -0
- package/src/tools/web-automation.ts +518 -0
- package/src/types/index.ts +132 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Desktop Automation Tools
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { registerDesktopTools } from './desktop-automation';
|
|
6
|
+
import { ToolRegistry } from './tool-registry';
|
|
7
|
+
import { MockCascadeMcpClient } from '../test-utils';
|
|
8
|
+
|
|
9
|
+
describe('Desktop Automation Tools', () => {
|
|
10
|
+
let registry: ToolRegistry;
|
|
11
|
+
let mockClient: MockCascadeMcpClient;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
registry = new ToolRegistry();
|
|
15
|
+
mockClient = new MockCascadeMcpClient();
|
|
16
|
+
registerDesktopTools(registry, mockClient as any);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('cascade_click_element', () => {
|
|
20
|
+
test('should be registered', () => {
|
|
21
|
+
expect(registry.has('cascade_click_element')).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('should click element by ID', async () => {
|
|
25
|
+
// Arrange
|
|
26
|
+
mockClient.registerMockTool('click_element', () => ({ success: true }));
|
|
27
|
+
|
|
28
|
+
// Act
|
|
29
|
+
const result = await registry.call('cascade_click_element', {
|
|
30
|
+
platform_source: 'WINDOWS',
|
|
31
|
+
id: 'button1'
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Assert
|
|
35
|
+
expect(mockClient.callTool).toHaveBeenCalledWith('click_element', {
|
|
36
|
+
platform_source: 'WINDOWS',
|
|
37
|
+
id: 'button1'
|
|
38
|
+
});
|
|
39
|
+
expect(result.content[0].text).toContain('success');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should handle click failure', async () => {
|
|
43
|
+
// Arrange
|
|
44
|
+
mockClient.simulateError(new Error('Element not found'));
|
|
45
|
+
|
|
46
|
+
// Act
|
|
47
|
+
const result = await registry.call('cascade_click_element', {
|
|
48
|
+
platform_source: 'WINDOWS',
|
|
49
|
+
id: 'nonexistent'
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Assert
|
|
53
|
+
expect(result.isError).toBe(true);
|
|
54
|
+
expect(result.content[0].text).toContain('Element not found');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('should require platform_source', async () => {
|
|
58
|
+
// Act
|
|
59
|
+
const result = await registry.call('cascade_click_element', {
|
|
60
|
+
id: 'button1'
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Assert
|
|
64
|
+
expect(result.isError).toBe(true);
|
|
65
|
+
expect(result.content[0].text).toContain('platform_source is required');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('cascade_type_text', () => {
|
|
70
|
+
test('should be registered', () => {
|
|
71
|
+
expect(registry.has('cascade_type_text')).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('should type text into element', async () => {
|
|
75
|
+
// Arrange
|
|
76
|
+
mockClient.registerMockTool('type_text', () => ({ success: true }));
|
|
77
|
+
|
|
78
|
+
// Act
|
|
79
|
+
await registry.call('cascade_type_text', {
|
|
80
|
+
selector: { platform_source: 'WINDOWS', id: 'input1' },
|
|
81
|
+
text: 'Hello World'
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Assert
|
|
85
|
+
expect(mockClient.callTool).toHaveBeenCalledWith('type_text', {
|
|
86
|
+
selector: { platform_source: 'WINDOWS', id: 'input1' },
|
|
87
|
+
text: 'Hello World'
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('should require selector', async () => {
|
|
92
|
+
// Act
|
|
93
|
+
const result = await registry.call('cascade_type_text', {
|
|
94
|
+
text: 'Hello'
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Assert
|
|
98
|
+
expect(result.isError).toBe(true);
|
|
99
|
+
expect(result.content[0].text).toContain('selector');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('should require text', async () => {
|
|
103
|
+
// Act
|
|
104
|
+
const result = await registry.call('cascade_type_text', {
|
|
105
|
+
selector: { platform_source: 'WINDOWS' }
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Assert
|
|
109
|
+
expect(result.isError).toBe(true);
|
|
110
|
+
expect(result.content[0].text).toContain('text is required');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('cascade_get_semantic_tree', () => {
|
|
115
|
+
test('should be registered', () => {
|
|
116
|
+
expect(registry.has('cascade_get_semantic_tree')).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('should return semantic tree', async () => {
|
|
120
|
+
// Arrange
|
|
121
|
+
mockClient.registerMockTool('get_semantic_tree', () => ({
|
|
122
|
+
elements: [
|
|
123
|
+
{ id: '1', name: 'Button', control_type: 'BUTTON' },
|
|
124
|
+
{ id: '2', name: 'Input', control_type: 'INPUT' }
|
|
125
|
+
]
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
// Act
|
|
129
|
+
const result = await registry.call('cascade_get_semantic_tree', {});
|
|
130
|
+
|
|
131
|
+
// Assert
|
|
132
|
+
expect(mockClient.callTool).toHaveBeenCalledWith('get_semantic_tree', {});
|
|
133
|
+
expect(result.content[0].text).toContain('Button');
|
|
134
|
+
expect(result.content[0].text).toContain('Input');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('should handle empty tree', async () => {
|
|
138
|
+
// Arrange
|
|
139
|
+
mockClient.registerMockTool('get_semantic_tree', () => ({
|
|
140
|
+
elements: []
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
// Act
|
|
144
|
+
const result = await registry.call('cascade_get_semantic_tree', {});
|
|
145
|
+
|
|
146
|
+
// Assert
|
|
147
|
+
expect(result.content[0].text).toContain('elements');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('cascade_get_screenshot', () => {
|
|
152
|
+
test('should be registered', () => {
|
|
153
|
+
expect(registry.has('cascade_get_screenshot')).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('should return embedded screenshot when small', async () => {
|
|
157
|
+
// Arrange
|
|
158
|
+
const smallImage = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
|
159
|
+
mockClient.registerMockTool('get_screenshot', () => ({
|
|
160
|
+
image: smallImage,
|
|
161
|
+
format: 'PNG',
|
|
162
|
+
marks: [{ element_id: '1', label: 'A' }]
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
// Act
|
|
166
|
+
const result = await registry.call('cascade_get_screenshot', {});
|
|
167
|
+
|
|
168
|
+
// Assert
|
|
169
|
+
expect(result.content[0].type).toBe('image');
|
|
170
|
+
expect(result.content[0].source.data).toBe(smallImage);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('should save large screenshot to disk', async () => {
|
|
174
|
+
// Arrange
|
|
175
|
+
const largeImage = 'A'.repeat(5000000); // 5MB base64 = ~3.75MB actual
|
|
176
|
+
mockClient.registerMockTool('get_screenshot', () => ({
|
|
177
|
+
image: largeImage,
|
|
178
|
+
format: 'PNG',
|
|
179
|
+
marks: []
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
// Act
|
|
183
|
+
const result = await registry.call('cascade_get_screenshot', {});
|
|
184
|
+
|
|
185
|
+
// Assert
|
|
186
|
+
expect(result.content[0].type).toBe('text');
|
|
187
|
+
expect(result.content[0].text).toContain('saved');
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('cascade_start_app', () => {
|
|
192
|
+
test('should be registered', () => {
|
|
193
|
+
expect(registry.has('cascade_start_app')).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('should start application', async () => {
|
|
197
|
+
// Arrange
|
|
198
|
+
mockClient.registerMockTool('start_app', () => ({ success: true }));
|
|
199
|
+
|
|
200
|
+
// Act
|
|
201
|
+
const result = await registry.call('cascade_start_app', {
|
|
202
|
+
app_name: 'notepad'
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Assert
|
|
206
|
+
expect(mockClient.callTool).toHaveBeenCalledWith('start_app', {
|
|
207
|
+
app_name: 'notepad'
|
|
208
|
+
});
|
|
209
|
+
expect(result.content[0].text).toContain('success');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('should require app_name', async () => {
|
|
213
|
+
// Act
|
|
214
|
+
const result = await registry.call('cascade_start_app', {});
|
|
215
|
+
|
|
216
|
+
// Assert
|
|
217
|
+
expect(result.isError).toBe(true);
|
|
218
|
+
expect(result.content[0].text).toContain('app_name is required');
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('cascade_hover_element', () => {
|
|
223
|
+
test('should be registered', () => {
|
|
224
|
+
expect(registry.has('cascade_hover_element')).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('should hover over element', async () => {
|
|
228
|
+
// Arrange
|
|
229
|
+
mockClient.registerMockTool('hover_element', () => ({ success: true }));
|
|
230
|
+
|
|
231
|
+
// Act
|
|
232
|
+
await registry.call('cascade_hover_element', {
|
|
233
|
+
selector: { platform_source: 'WINDOWS', id: 'button1' }
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Assert
|
|
237
|
+
expect(mockClient.callTool).toHaveBeenCalledWith('hover_element', {
|
|
238
|
+
selector: { platform_source: 'WINDOWS', id: 'button1' }
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('cascade_focus_element', () => {
|
|
244
|
+
test('should be registered', () => {
|
|
245
|
+
expect(registry.has('cascade_focus_element')).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('should focus on element', async () => {
|
|
249
|
+
// Arrange
|
|
250
|
+
mockClient.registerMockTool('focus_element', () => ({ success: true }));
|
|
251
|
+
|
|
252
|
+
// Act
|
|
253
|
+
await registry.call('cascade_focus_element', {
|
|
254
|
+
selector: { platform_source: 'WINDOWS', id: 'input1' }
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Assert
|
|
258
|
+
expect(mockClient.callTool).toHaveBeenCalledWith('focus_element', {
|
|
259
|
+
selector: { platform_source: 'WINDOWS', id: 'input1' }
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('cascade_scroll_element', () => {
|
|
265
|
+
test('should be registered', () => {
|
|
266
|
+
expect(registry.has('cascade_scroll_element')).toBe(true);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('should scroll element', async () => {
|
|
270
|
+
// Arrange
|
|
271
|
+
mockClient.registerMockTool('scroll_element', () => ({ success: true }));
|
|
272
|
+
|
|
273
|
+
// Act
|
|
274
|
+
await registry.call('cascade_scroll_element', {
|
|
275
|
+
selector: { platform_source: 'WINDOWS', id: 'scrollable' }
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Assert
|
|
279
|
+
expect(mockClient.callTool).toHaveBeenCalledWith('scroll_element', {
|
|
280
|
+
selector: { platform_source: 'WINDOWS', id: 'scrollable' }
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('cascade_wait_visible', () => {
|
|
286
|
+
test('should be registered', () => {
|
|
287
|
+
expect(registry.has('cascade_wait_visible')).toBe(true);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('should wait for element visibility', async () => {
|
|
291
|
+
// Arrange
|
|
292
|
+
mockClient.registerMockTool('wait_visible', () => ({ success: true }));
|
|
293
|
+
|
|
294
|
+
// Act
|
|
295
|
+
await registry.call('cascade_wait_visible', {
|
|
296
|
+
selector: { platform_source: 'WINDOWS', id: 'loading' }
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Assert
|
|
300
|
+
expect(mockClient.callTool).toHaveBeenCalledWith('wait_visible', {
|
|
301
|
+
selector: { platform_source: 'WINDOWS', id: 'loading' }
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
});
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desktop Automation Tools
|
|
3
|
+
*
|
|
4
|
+
* 9 tools for controlling Windows desktop applications
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ToolRegistry } from './tool-registry';
|
|
8
|
+
import { CascadeMcpClient } from '../cascade-client';
|
|
9
|
+
import { ToolResponse, CascadePluginConfig } from '../types';
|
|
10
|
+
import { errorResponse, formatSuccess, imageResponse } from './response-helpers';
|
|
11
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { homedir } from 'os';
|
|
14
|
+
|
|
15
|
+
// Max size for embedded images (4MB)
|
|
16
|
+
const MAX_EMBED_SIZE = 4 * 1024 * 1024;
|
|
17
|
+
|
|
18
|
+
export function registerDesktopTools(
|
|
19
|
+
registry: ToolRegistry,
|
|
20
|
+
client: CascadeMcpClient,
|
|
21
|
+
config?: CascadePluginConfig
|
|
22
|
+
): void {
|
|
23
|
+
const screenshotMode = config?.screenshotMode || 'auto';
|
|
24
|
+
const screenshotDir = config?.screenshotDir || join(homedir(), '.openclaw', 'screenshots');
|
|
25
|
+
|
|
26
|
+
// 1. cascade_click_element
|
|
27
|
+
registry.register({
|
|
28
|
+
name: 'cascade_click_element',
|
|
29
|
+
description: 'Click on a UI element on the desktop or in an application',
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
platform_source: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
enum: ['WINDOWS', 'JAVA', 'WEB'],
|
|
36
|
+
description: 'Platform type (Windows desktop, Java app, or web)'
|
|
37
|
+
},
|
|
38
|
+
id: { type: 'string', description: 'Element ID if known' },
|
|
39
|
+
name: { type: 'string', description: 'Element name/label' },
|
|
40
|
+
control_type: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
enum: ['BUTTON', 'INPUT', 'COMBO', 'MENU', 'TREE', 'TABLE', 'CUSTOM'],
|
|
43
|
+
description: 'Control type'
|
|
44
|
+
},
|
|
45
|
+
path: {
|
|
46
|
+
type: 'array',
|
|
47
|
+
items: { type: 'string' },
|
|
48
|
+
description: 'Path components for navigation'
|
|
49
|
+
},
|
|
50
|
+
index: { type: 'integer', description: 'Index if multiple matches' },
|
|
51
|
+
text_hint: { type: 'string', description: 'Text hint for element' }
|
|
52
|
+
},
|
|
53
|
+
required: ['platform_source']
|
|
54
|
+
},
|
|
55
|
+
handler: async (args): Promise<ToolResponse> => {
|
|
56
|
+
try {
|
|
57
|
+
if (!args.platform_source) {
|
|
58
|
+
return errorResponse('platform_source is required');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result = await client.callTool('click_element', args);
|
|
62
|
+
return formatSuccess(result);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return errorResponse(
|
|
65
|
+
error instanceof Error ? error.message : 'Failed to click element',
|
|
66
|
+
'Try using cascade_get_semantic_tree first to identify the correct element'
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 2. cascade_type_text
|
|
73
|
+
registry.register({
|
|
74
|
+
name: 'cascade_type_text',
|
|
75
|
+
description: 'Type text into a UI element (input field, text area, etc.)',
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
selector: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
description: 'Element selector',
|
|
82
|
+
properties: {
|
|
83
|
+
platform_source: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
enum: ['WINDOWS', 'JAVA', 'WEB']
|
|
86
|
+
},
|
|
87
|
+
id: { type: 'string' },
|
|
88
|
+
name: { type: 'string' },
|
|
89
|
+
control_type: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
enum: ['BUTTON', 'INPUT', 'COMBO', 'MENU', 'TREE', 'TABLE', 'CUSTOM']
|
|
92
|
+
},
|
|
93
|
+
path: { type: 'array', items: { type: 'string' } },
|
|
94
|
+
index: { type: 'integer' },
|
|
95
|
+
text_hint: { type: 'string' }
|
|
96
|
+
},
|
|
97
|
+
required: ['platform_source']
|
|
98
|
+
},
|
|
99
|
+
text: {
|
|
100
|
+
type: 'string',
|
|
101
|
+
description: 'Text to type'
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
required: ['selector', 'text']
|
|
105
|
+
},
|
|
106
|
+
handler: async (args): Promise<ToolResponse> => {
|
|
107
|
+
try {
|
|
108
|
+
if (!args.selector) {
|
|
109
|
+
return errorResponse('selector is required');
|
|
110
|
+
}
|
|
111
|
+
if (!args.text) {
|
|
112
|
+
return errorResponse('text is required');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const result = await client.callTool('type_text', args);
|
|
116
|
+
return formatSuccess(result);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return errorResponse(
|
|
119
|
+
error instanceof Error ? error.message : 'Failed to type text'
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// 3. cascade_get_semantic_tree
|
|
126
|
+
registry.register({
|
|
127
|
+
name: 'cascade_get_semantic_tree',
|
|
128
|
+
description: 'Get the UI element structure of the current window or application',
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: 'object',
|
|
131
|
+
properties: {}
|
|
132
|
+
},
|
|
133
|
+
handler: async (): Promise<ToolResponse> => {
|
|
134
|
+
try {
|
|
135
|
+
const result = await client.callTool('get_semantic_tree', {});
|
|
136
|
+
return formatSuccess(result);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return errorResponse(
|
|
139
|
+
error instanceof Error ? error.message : 'Failed to get semantic tree'
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// 4. cascade_get_screenshot
|
|
146
|
+
registry.register({
|
|
147
|
+
name: 'cascade_get_screenshot',
|
|
148
|
+
description: 'Capture a screenshot of the current screen with element annotations',
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: {}
|
|
152
|
+
},
|
|
153
|
+
handler: async (): Promise<ToolResponse> => {
|
|
154
|
+
try {
|
|
155
|
+
const result = await client.callTool('get_screenshot', {});
|
|
156
|
+
|
|
157
|
+
if (!result.image) {
|
|
158
|
+
return errorResponse('No image data received');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const imageSize = Buffer.from(result.image, 'base64').length;
|
|
162
|
+
const shouldEmbed = screenshotMode === 'embed' ||
|
|
163
|
+
(screenshotMode === 'auto' && imageSize < MAX_EMBED_SIZE);
|
|
164
|
+
|
|
165
|
+
if (shouldEmbed) {
|
|
166
|
+
return imageResponse(
|
|
167
|
+
result.image,
|
|
168
|
+
result.format?.toLowerCase() === 'jpeg' ? 'jpeg' : 'png',
|
|
169
|
+
`Screenshot captured (${(imageSize / 1024).toFixed(1)} KB)`
|
|
170
|
+
);
|
|
171
|
+
} else {
|
|
172
|
+
// Save to disk
|
|
173
|
+
await mkdir(screenshotDir, { recursive: true });
|
|
174
|
+
const filename = `cascade-${Date.now()}.${result.format?.toLowerCase() || 'png'}`;
|
|
175
|
+
const filepath = join(screenshotDir, filename);
|
|
176
|
+
await writeFile(filepath, Buffer.from(result.image, 'base64'));
|
|
177
|
+
|
|
178
|
+
return formatSuccess({
|
|
179
|
+
saved_to: filepath,
|
|
180
|
+
size_kb: (imageSize / 1024).toFixed(1),
|
|
181
|
+
marks: result.marks
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return errorResponse(
|
|
186
|
+
error instanceof Error ? error.message : 'Failed to capture screenshot'
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// 5. cascade_start_app
|
|
193
|
+
registry.register({
|
|
194
|
+
name: 'cascade_start_app',
|
|
195
|
+
description: 'Start an application by name',
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: {
|
|
199
|
+
app_name: {
|
|
200
|
+
type: 'string',
|
|
201
|
+
description: 'Application name or executable (e.g., notepad, calc, excel)'
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
required: ['app_name']
|
|
205
|
+
},
|
|
206
|
+
handler: async (args): Promise<ToolResponse> => {
|
|
207
|
+
try {
|
|
208
|
+
if (!args.app_name) {
|
|
209
|
+
return errorResponse('app_name is required');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const result = await client.callTool('start_app', args);
|
|
213
|
+
return formatSuccess(result);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
return errorResponse(
|
|
216
|
+
error instanceof Error ? error.message : 'Failed to start application',
|
|
217
|
+
'Verify the app name is correct and the application is installed'
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// 6. cascade_hover_element
|
|
224
|
+
registry.register({
|
|
225
|
+
name: 'cascade_hover_element',
|
|
226
|
+
description: 'Hover over a UI element',
|
|
227
|
+
inputSchema: {
|
|
228
|
+
type: 'object',
|
|
229
|
+
properties: {
|
|
230
|
+
selector: {
|
|
231
|
+
type: 'object',
|
|
232
|
+
description: 'Element selector',
|
|
233
|
+
properties: {
|
|
234
|
+
platform_source: { type: 'string', enum: ['WINDOWS', 'JAVA', 'WEB'] },
|
|
235
|
+
id: { type: 'string' },
|
|
236
|
+
name: { type: 'string' },
|
|
237
|
+
control_type: { type: 'string' },
|
|
238
|
+
path: { type: 'array', items: { type: 'string' } },
|
|
239
|
+
index: { type: 'integer' },
|
|
240
|
+
text_hint: { type: 'string' }
|
|
241
|
+
},
|
|
242
|
+
required: ['platform_source']
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
required: ['selector']
|
|
246
|
+
},
|
|
247
|
+
handler: async (args): Promise<ToolResponse> => {
|
|
248
|
+
try {
|
|
249
|
+
const result = await client.callTool('hover_element', args);
|
|
250
|
+
return formatSuccess(result);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
return errorResponse(
|
|
253
|
+
error instanceof Error ? error.message : 'Failed to hover element'
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// 7. cascade_focus_element
|
|
260
|
+
registry.register({
|
|
261
|
+
name: 'cascade_focus_element',
|
|
262
|
+
description: 'Focus on a UI element',
|
|
263
|
+
inputSchema: {
|
|
264
|
+
type: 'object',
|
|
265
|
+
properties: {
|
|
266
|
+
selector: {
|
|
267
|
+
type: 'object',
|
|
268
|
+
description: 'Element selector',
|
|
269
|
+
properties: {
|
|
270
|
+
platform_source: { type: 'string', enum: ['WINDOWS', 'JAVA', 'WEB'] },
|
|
271
|
+
id: { type: 'string' },
|
|
272
|
+
name: { type: 'string' },
|
|
273
|
+
control_type: { type: 'string' },
|
|
274
|
+
path: { type: 'array', items: { type: 'string' } },
|
|
275
|
+
index: { type: 'integer' },
|
|
276
|
+
text_hint: { type: 'string' }
|
|
277
|
+
},
|
|
278
|
+
required: ['platform_source']
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
required: ['selector']
|
|
282
|
+
},
|
|
283
|
+
handler: async (args): Promise<ToolResponse> => {
|
|
284
|
+
try {
|
|
285
|
+
const result = await client.callTool('focus_element', args);
|
|
286
|
+
return formatSuccess(result);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
return errorResponse(
|
|
289
|
+
error instanceof Error ? error.message : 'Failed to focus element'
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// 8. cascade_scroll_element
|
|
296
|
+
registry.register({
|
|
297
|
+
name: 'cascade_scroll_element',
|
|
298
|
+
description: 'Scroll a UI element',
|
|
299
|
+
inputSchema: {
|
|
300
|
+
type: 'object',
|
|
301
|
+
properties: {
|
|
302
|
+
selector: {
|
|
303
|
+
type: 'object',
|
|
304
|
+
description: 'Element selector',
|
|
305
|
+
properties: {
|
|
306
|
+
platform_source: { type: 'string', enum: ['WINDOWS', 'JAVA', 'WEB'] },
|
|
307
|
+
id: { type: 'string' },
|
|
308
|
+
name: { type: 'string' },
|
|
309
|
+
control_type: { type: 'string' },
|
|
310
|
+
path: { type: 'array', items: { type: 'string' } },
|
|
311
|
+
index: { type: 'integer' },
|
|
312
|
+
text_hint: { type: 'string' }
|
|
313
|
+
},
|
|
314
|
+
required: ['platform_source']
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
required: ['selector']
|
|
318
|
+
},
|
|
319
|
+
handler: async (args): Promise<ToolResponse> => {
|
|
320
|
+
try {
|
|
321
|
+
const result = await client.callTool('scroll_element', args);
|
|
322
|
+
return formatSuccess(result);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
return errorResponse(
|
|
325
|
+
error instanceof Error ? error.message : 'Failed to scroll element'
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// 9. cascade_wait_visible
|
|
332
|
+
registry.register({
|
|
333
|
+
name: 'cascade_wait_visible',
|
|
334
|
+
description: 'Wait for a UI element to become visible',
|
|
335
|
+
inputSchema: {
|
|
336
|
+
type: 'object',
|
|
337
|
+
properties: {
|
|
338
|
+
selector: {
|
|
339
|
+
type: 'object',
|
|
340
|
+
description: 'Element selector',
|
|
341
|
+
properties: {
|
|
342
|
+
platform_source: { type: 'string', enum: ['WINDOWS', 'JAVA', 'WEB'] },
|
|
343
|
+
id: { type: 'string' },
|
|
344
|
+
name: { type: 'string' },
|
|
345
|
+
control_type: { type: 'string' },
|
|
346
|
+
path: { type: 'array', items: { type: 'string' } },
|
|
347
|
+
index: { type: 'integer' },
|
|
348
|
+
text_hint: { type: 'string' }
|
|
349
|
+
},
|
|
350
|
+
required: ['platform_source']
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
required: ['selector']
|
|
354
|
+
},
|
|
355
|
+
handler: async (args): Promise<ToolResponse> => {
|
|
356
|
+
try {
|
|
357
|
+
const result = await client.callTool('wait_visible', args);
|
|
358
|
+
return formatSuccess(result);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
return errorResponse(
|
|
361
|
+
error instanceof Error ? error.message : 'Failed to wait for element'
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools Index
|
|
3
|
+
*
|
|
4
|
+
* Export all tool registration functions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { registerDesktopTools } from './desktop-automation';
|
|
8
|
+
export { registerWebTools } from './web-automation';
|
|
9
|
+
export { registerApiTools } from './api-tools';
|
|
10
|
+
export { registerSandboxTools } from './sandbox-tools';
|
|
11
|
+
export { registerA2ATools } from './a2a-tools';
|
|
12
|
+
// Note: ToolRegistry class is not exported from main index to avoid conflict with ToolRegistry interface
|
|
13
|
+
export * from './response-helpers';
|