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.
Files changed (98) hide show
  1. package/PHASE1_SUMMARY.md +191 -0
  2. package/PHASE3_SUMMARY.md +195 -0
  3. package/README.md +43 -0
  4. package/dist/a2a-client.d.ts +17 -0
  5. package/dist/a2a-client.d.ts.map +1 -0
  6. package/dist/a2a-client.js +47 -0
  7. package/dist/a2a-client.js.map +1 -0
  8. package/dist/cascade-client.d.ts +53 -0
  9. package/dist/cascade-client.d.ts.map +1 -0
  10. package/dist/cascade-client.js +179 -0
  11. package/dist/cascade-client.js.map +1 -0
  12. package/dist/config.d.ts +26 -0
  13. package/dist/config.d.ts.map +1 -0
  14. package/dist/config.js +116 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/index.d.ts +29 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +136 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/python-manager.d.ts +59 -0
  21. package/dist/python-manager.d.ts.map +1 -0
  22. package/dist/python-manager.js +190 -0
  23. package/dist/python-manager.js.map +1 -0
  24. package/dist/test-utils/helpers.d.ts +20 -0
  25. package/dist/test-utils/helpers.d.ts.map +1 -0
  26. package/dist/test-utils/helpers.js +89 -0
  27. package/dist/test-utils/helpers.js.map +1 -0
  28. package/dist/test-utils/index.d.ts +3 -0
  29. package/dist/test-utils/index.d.ts.map +1 -0
  30. package/dist/test-utils/index.js +19 -0
  31. package/dist/test-utils/index.js.map +1 -0
  32. package/dist/test-utils/mocks.d.ts +51 -0
  33. package/dist/test-utils/mocks.d.ts.map +1 -0
  34. package/dist/test-utils/mocks.js +84 -0
  35. package/dist/test-utils/mocks.js.map +1 -0
  36. package/dist/tools/a2a-tools.d.ts +9 -0
  37. package/dist/tools/a2a-tools.d.ts.map +1 -0
  38. package/dist/tools/a2a-tools.js +147 -0
  39. package/dist/tools/a2a-tools.js.map +1 -0
  40. package/dist/tools/api-tools.d.ts +9 -0
  41. package/dist/tools/api-tools.d.ts.map +1 -0
  42. package/dist/tools/api-tools.js +102 -0
  43. package/dist/tools/api-tools.js.map +1 -0
  44. package/dist/tools/desktop-automation.d.ts +10 -0
  45. package/dist/tools/desktop-automation.d.ts.map +1 -0
  46. package/dist/tools/desktop-automation.js +330 -0
  47. package/dist/tools/desktop-automation.js.map +1 -0
  48. package/dist/tools/index.d.ts +12 -0
  49. package/dist/tools/index.d.ts.map +1 -0
  50. package/dist/tools/index.js +35 -0
  51. package/dist/tools/index.js.map +1 -0
  52. package/dist/tools/response-helpers.d.ts +25 -0
  53. package/dist/tools/response-helpers.d.ts.map +1 -0
  54. package/dist/tools/response-helpers.js +71 -0
  55. package/dist/tools/response-helpers.js.map +1 -0
  56. package/dist/tools/sandbox-tools.d.ts +9 -0
  57. package/dist/tools/sandbox-tools.d.ts.map +1 -0
  58. package/dist/tools/sandbox-tools.js +79 -0
  59. package/dist/tools/sandbox-tools.js.map +1 -0
  60. package/dist/tools/tool-registry.d.ts +34 -0
  61. package/dist/tools/tool-registry.d.ts.map +1 -0
  62. package/dist/tools/tool-registry.js +50 -0
  63. package/dist/tools/tool-registry.js.map +1 -0
  64. package/dist/tools/web-automation.d.ts +9 -0
  65. package/dist/tools/web-automation.d.ts.map +1 -0
  66. package/dist/tools/web-automation.js +471 -0
  67. package/dist/tools/web-automation.js.map +1 -0
  68. package/dist/types/index.d.ts +111 -0
  69. package/dist/types/index.d.ts.map +1 -0
  70. package/dist/types/index.js +38 -0
  71. package/dist/types/index.js.map +1 -0
  72. package/jest.setup.js +19 -0
  73. package/openclaw-cascade-plugin-1.0.0.tgz +0 -0
  74. package/openclaw.plugin.json +116 -0
  75. package/package.json +74 -0
  76. package/src/a2a-client.ts +66 -0
  77. package/src/cascade-client.test.ts +400 -0
  78. package/src/cascade-client.ts +198 -0
  79. package/src/config.test.ts +194 -0
  80. package/src/config.ts +135 -0
  81. package/src/index.ts +164 -0
  82. package/src/python-manager.test.ts +187 -0
  83. package/src/python-manager.ts +230 -0
  84. package/src/test-utils/helpers.ts +107 -0
  85. package/src/test-utils/index.ts +2 -0
  86. package/src/test-utils/mocks.ts +101 -0
  87. package/src/tools/a2a-tools.ts +162 -0
  88. package/src/tools/api-tools.ts +110 -0
  89. package/src/tools/desktop-automation.test.ts +305 -0
  90. package/src/tools/desktop-automation.ts +366 -0
  91. package/src/tools/index.ts +13 -0
  92. package/src/tools/response-helpers.ts +78 -0
  93. package/src/tools/sandbox-tools.ts +83 -0
  94. package/src/tools/tool-registry.ts +51 -0
  95. package/src/tools/web-automation.test.ts +177 -0
  96. package/src/tools/web-automation.ts +518 -0
  97. package/src/types/index.ts +132 -0
  98. package/tsconfig.json +27 -0
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Base tool response helpers
3
+ */
4
+
5
+ import { ToolResponse } from '../types';
6
+
7
+ /**
8
+ * Create a successful text response
9
+ */
10
+ export function successResponse(text: string): ToolResponse {
11
+ return {
12
+ content: [{ type: 'text', text }]
13
+ };
14
+ }
15
+
16
+ /**
17
+ * Create a successful JSON response
18
+ */
19
+ export function jsonResponse(data: any): ToolResponse {
20
+ return {
21
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Create an image response with optional text
27
+ */
28
+ export function imageResponse(
29
+ base64Data: string,
30
+ format: 'png' | 'jpeg' = 'png',
31
+ text?: string
32
+ ): ToolResponse {
33
+ const content: any[] = [
34
+ {
35
+ type: 'image',
36
+ source: {
37
+ type: 'base64',
38
+ media_type: `image/${format}`,
39
+ data: base64Data
40
+ }
41
+ }
42
+ ];
43
+
44
+ if (text) {
45
+ content.push({ type: 'text', text });
46
+ }
47
+
48
+ return { content };
49
+ }
50
+
51
+ /**
52
+ * Create an error response
53
+ */
54
+ export function errorResponse(message: string, suggestion?: string): ToolResponse {
55
+ const response: any = {
56
+ success: false,
57
+ error: message
58
+ };
59
+
60
+ if (suggestion) {
61
+ response.suggestion = suggestion;
62
+ }
63
+
64
+ return {
65
+ content: [{ type: 'text', text: JSON.stringify(response, null, 2) }],
66
+ isError: true
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Format a successful tool result
72
+ */
73
+ export function formatSuccess(result: any): ToolResponse {
74
+ return jsonResponse({
75
+ success: true,
76
+ ...result
77
+ });
78
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Sandbox Tools
3
+ *
4
+ * 1 tool for executing Python code in sandbox
5
+ */
6
+
7
+ import { ToolRegistry } from './tool-registry';
8
+ import { CascadeMcpClient } from '../cascade-client';
9
+ import { ToolResponse } from '../types';
10
+ import { errorResponse, formatSuccess } from './response-helpers';
11
+
12
+ export function registerSandboxTools(registry: ToolRegistry, client: CascadeMcpClient): void {
13
+ registry.register({
14
+ name: 'cascade_execute_sandbox_skill',
15
+ description: 'Execute a Python sandbox skill for programmatic file automation',
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: {
19
+ skill_id: {
20
+ type: 'string',
21
+ description: 'Skill ID to execute'
22
+ },
23
+ task: {
24
+ type: 'string',
25
+ description: 'Task description to guide execution'
26
+ },
27
+ inputs: {
28
+ type: 'object',
29
+ description: 'Runtime inputs for the operation',
30
+ additionalProperties: true
31
+ },
32
+ files: {
33
+ type: 'array',
34
+ description: 'Files to copy in/out of sandbox',
35
+ items: {
36
+ type: 'object',
37
+ properties: {
38
+ name: { type: 'string' },
39
+ local_path: { type: 'string' },
40
+ sandbox_path: { type: 'string' },
41
+ mode: { type: 'string', enum: ['input', 'output', 'inout'] }
42
+ },
43
+ required: ['name', 'local_path', 'mode']
44
+ }
45
+ },
46
+ local_path: {
47
+ type: 'string',
48
+ description: 'Primary file path (convenience when using single file)'
49
+ },
50
+ python_code: {
51
+ type: 'string',
52
+ description: 'Optional Python code to execute (auto-generated if not provided)'
53
+ }
54
+ },
55
+ required: ['skill_id', 'task']
56
+ },
57
+ handler: async (args): Promise<ToolResponse> => {
58
+ try {
59
+ if (!args.skill_id) {
60
+ return errorResponse('skill_id is required');
61
+ }
62
+ if (!args.task) {
63
+ return errorResponse('task is required');
64
+ }
65
+
66
+ const result = await client.callTool('execute_sandbox_skill', {
67
+ skill_id: args.skill_id,
68
+ task: args.task,
69
+ inputs: args.inputs || {},
70
+ files: args.files || [],
71
+ local_path: args.local_path,
72
+ python_code: args.python_code
73
+ });
74
+ return formatSuccess(result);
75
+ } catch (error) {
76
+ return errorResponse(
77
+ error instanceof Error ? error.message : 'Failed to execute sandbox skill',
78
+ 'Check that the skill exists and E2B_API_KEY is set'
79
+ );
80
+ }
81
+ }
82
+ });
83
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Tool Registry for managing OpenClaw tools
3
+ */
4
+
5
+ import { ToolSchema, ToolHandler, ToolResponse } from '../types';
6
+
7
+ export class ToolRegistry {
8
+ private tools = new Map<string, ToolSchema & { handler: ToolHandler }>();
9
+
10
+ /**
11
+ * Register a tool with the registry
12
+ */
13
+ register(tool: ToolSchema & { handler: ToolHandler }): void {
14
+ if (this.tools.has(tool.name)) {
15
+ throw new Error(`Tool ${tool.name} is already registered`);
16
+ }
17
+ this.tools.set(tool.name, tool);
18
+ }
19
+
20
+ /**
21
+ * Get all registered tools
22
+ */
23
+ getAll(): Array<ToolSchema & { handler: ToolHandler }> {
24
+ return Array.from(this.tools.values());
25
+ }
26
+
27
+ /**
28
+ * Get a specific tool by name
29
+ */
30
+ get(name: string): (ToolSchema & { handler: ToolHandler }) | undefined {
31
+ return this.tools.get(name);
32
+ }
33
+
34
+ /**
35
+ * Check if a tool exists
36
+ */
37
+ has(name: string): boolean {
38
+ return this.tools.has(name);
39
+ }
40
+
41
+ /**
42
+ * Call a tool by name with arguments
43
+ */
44
+ async call(name: string, args: Record<string, any>): Promise<ToolResponse> {
45
+ const tool = this.get(name);
46
+ if (!tool) {
47
+ throw new Error(`Tool ${name} not found`);
48
+ }
49
+ return tool.handler(args);
50
+ }
51
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Tests for Web Automation Tools (Playwright)
3
+ */
4
+
5
+ import { registerWebTools } from './web-automation';
6
+ import { ToolRegistry } from './tool-registry';
7
+ import { MockCascadeMcpClient } from '../test-utils';
8
+
9
+ describe('Web Automation Tools', () => {
10
+ let registry: ToolRegistry;
11
+ let mockClient: MockCascadeMcpClient;
12
+
13
+ beforeEach(() => {
14
+ registry = new ToolRegistry();
15
+ mockClient = new MockCascadeMcpClient();
16
+ registerWebTools(registry, mockClient as any);
17
+ });
18
+
19
+ describe('Navigation Tools', () => {
20
+ test('cascade_pw_goto should navigate to URL', async () => {
21
+ mockClient.registerMockTool('pw_goto', () => ({ success: true }));
22
+ await registry.call('cascade_pw_goto', { url: 'https://example.com' });
23
+ expect(mockClient.callTool).toHaveBeenCalledWith('pw_goto', {
24
+ url: 'https://example.com',
25
+ wait_until: 'networkidle'
26
+ });
27
+ });
28
+
29
+ test('cascade_pw_back should go back', async () => {
30
+ mockClient.registerMockTool('pw_back', () => ({ success: true }));
31
+ await registry.call('cascade_pw_back', {});
32
+ expect(mockClient.callTool).toHaveBeenCalledWith('pw_back', {});
33
+ });
34
+
35
+ test('cascade_pw_forward should go forward', async () => {
36
+ mockClient.registerMockTool('pw_forward', () => ({ success: true }));
37
+ await registry.call('cascade_pw_forward', {});
38
+ expect(mockClient.callTool).toHaveBeenCalledWith('pw_forward', {});
39
+ });
40
+
41
+ test('cascade_pw_reload should reload page', async () => {
42
+ mockClient.registerMockTool('pw_reload', () => ({ success: true }));
43
+ await registry.call('cascade_pw_reload', {});
44
+ expect(mockClient.callTool).toHaveBeenCalledWith('pw_reload', { wait_until: 'networkidle' });
45
+ });
46
+
47
+ test('cascade_pw_wait_for_url should wait for URL', async () => {
48
+ mockClient.registerMockTool('pw_wait_for_url', () => ({ success: true }));
49
+ await registry.call('cascade_pw_wait_for_url', { url: '**/success' });
50
+ expect(mockClient.callTool).toHaveBeenCalledWith('pw_wait_for_url', {
51
+ url: '**/success',
52
+ timeout_ms: 10000
53
+ });
54
+ });
55
+ });
56
+
57
+ describe('Locator Tools', () => {
58
+ test('cascade_pw_locator_count should count elements', async () => {
59
+ mockClient.registerMockTool('pw_locator_count', () => ({ count: 5 }));
60
+ const result = await registry.call('cascade_pw_locator_count', { selector: '.item' });
61
+ expect(mockClient.callTool).toHaveBeenCalledWith('pw_locator_count', { selector: '.item' });
62
+ expect(result.content[0].text).toContain('5');
63
+ });
64
+
65
+ test('cascade_pw_locator_text should get element text', async () => {
66
+ mockClient.registerMockTool('pw_locator_text', () => ({ text: 'Hello World' }));
67
+ const result = await registry.call('cascade_pw_locator_text', { selector: 'h1' });
68
+ expect(result.content[0].text).toContain('Hello World');
69
+ });
70
+ });
71
+
72
+ describe('Interaction Tools', () => {
73
+ test('cascade_pw_click should click element', async () => {
74
+ mockClient.registerMockTool('pw_click', () => ({ success: true }));
75
+ await registry.call('cascade_pw_click', { selector: '#submit' });
76
+ expect(mockClient.callTool).toHaveBeenCalledWith('pw_click', {
77
+ selector: '#submit',
78
+ timeout_ms: 8000
79
+ });
80
+ });
81
+
82
+ test('cascade_pw_fill should fill form field', async () => {
83
+ mockClient.registerMockTool('pw_fill', () => ({ success: true }));
84
+ await registry.call('cascade_pw_fill', { selector: '#email', text: 'test@example.com' });
85
+ expect(mockClient.callTool).toHaveBeenCalledWith('pw_fill', {
86
+ selector: '#email',
87
+ text: 'test@example.com',
88
+ timeout_ms: 8000
89
+ });
90
+ });
91
+
92
+ test('cascade_pw_fill should require text', async () => {
93
+ const result = await registry.call('cascade_pw_fill', { selector: '#email' });
94
+ expect(result.isError).toBe(true);
95
+ expect(result.content[0].text).toContain('text');
96
+ });
97
+
98
+ test('cascade_pw_press should press key', async () => {
99
+ mockClient.registerMockTool('pw_press', () => ({ success: true }));
100
+ await registry.call('cascade_pw_press', { selector: '#search', key: 'Enter' });
101
+ expect(mockClient.callTool).toHaveBeenCalledWith('pw_press', {
102
+ selector: '#search',
103
+ key: 'Enter',
104
+ timeout_ms: 8000
105
+ });
106
+ });
107
+
108
+ test('cascade_pw_select_option should select dropdown option', async () => {
109
+ mockClient.registerMockTool('pw_select_option', () => ({ selected: ['option1'] }));
110
+ const result = await registry.call('cascade_pw_select_option', {
111
+ selector: '#country',
112
+ values: ['USA', 'Canada']
113
+ });
114
+ expect(result.content[0].text).toContain('selected');
115
+ });
116
+ });
117
+
118
+ describe('Evaluation Tools', () => {
119
+ test('cascade_pw_eval should evaluate JavaScript', async () => {
120
+ mockClient.registerMockTool('pw_eval', () => ({ result: { title: 'Page Title' } }));
121
+ const result = await registry.call('cascade_pw_eval', {
122
+ expression: 'document.title'
123
+ });
124
+ expect(result.content[0].text).toContain('Page Title');
125
+ });
126
+
127
+ test('cascade_pw_eval_on_selector should evaluate on element', async () => {
128
+ mockClient.registerMockTool('pw_eval_on_selector', () => ({ result: 'Hello' }));
129
+ const result = await registry.call('cascade_pw_eval_on_selector', {
130
+ selector: 'h1',
131
+ expression: 'el => el.textContent'
132
+ });
133
+ expect(result.content[0].text).toContain('Hello');
134
+ });
135
+
136
+ test('cascade_pw_list_frames should list frames', async () => {
137
+ mockClient.registerMockTool('pw_list_frames', () => ({
138
+ frames: [
139
+ { name: 'main', url: 'https://example.com' },
140
+ { name: 'iframe1', url: 'https://example.com/frame' }
141
+ ]
142
+ }));
143
+ const result = await registry.call('cascade_pw_list_frames', {});
144
+ expect(result.content[0].text).toContain('main');
145
+ expect(result.content[0].text).toContain('iframe1');
146
+ });
147
+ });
148
+
149
+ describe('Storage Tools', () => {
150
+ test('cascade_pw_get_cookies should return cookies', async () => {
151
+ mockClient.registerMockTool('pw_get_cookies', () => ({
152
+ cookies: [
153
+ { name: 'session', value: 'abc123', domain: '.example.com' }
154
+ ]
155
+ }));
156
+ const result = await registry.call('cascade_pw_get_cookies', {});
157
+ expect(result.content[0].text).toContain('session');
158
+ expect(result.content[0].text).toContain('abc123');
159
+ });
160
+ });
161
+
162
+ describe('Error Handling', () => {
163
+ test('should handle element not found', async () => {
164
+ mockClient.simulateError(new Error('Element not found'));
165
+ const result = await registry.call('cascade_pw_click', { selector: '#missing' });
166
+ expect(result.isError).toBe(true);
167
+ expect(result.content[0].text).toContain('Element not found');
168
+ });
169
+
170
+ test('should handle timeout', async () => {
171
+ mockClient.simulateError(new Error('Timeout exceeded'));
172
+ const result = await registry.call('cascade_pw_wait_for_url', { url: '**/slow' });
173
+ expect(result.isError).toBe(true);
174
+ expect(result.content[0].text).toContain('Timeout exceeded');
175
+ });
176
+ });
177
+ });