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,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python Manager - Handles Python environment detection and installation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { CascadePluginConfig } from './types';
|
|
8
|
+
|
|
9
|
+
export class PythonManager {
|
|
10
|
+
private readonly MIN_PYTHON_VERSION = 3.10;
|
|
11
|
+
|
|
12
|
+
constructor(private config: CascadePluginConfig) {}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Find or install Python for Cascade
|
|
16
|
+
* Priority: 1) Configured path 2) Auto-detect 3) Auto-install
|
|
17
|
+
*/
|
|
18
|
+
async findOrInstallPython(): Promise<string> {
|
|
19
|
+
// 1. Check configured path
|
|
20
|
+
if (this.config.cascadePythonPath) {
|
|
21
|
+
if (await this.isValidPython(this.config.cascadePythonPath)) {
|
|
22
|
+
return this.config.cascadePythonPath;
|
|
23
|
+
}
|
|
24
|
+
console.warn(`Configured Python path ${this.config.cascadePythonPath} is not valid, searching...`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 2. Auto-detect in common locations
|
|
28
|
+
const detected = await this.autoDetectPython();
|
|
29
|
+
if (detected) {
|
|
30
|
+
return detected;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 3. Auto-install
|
|
34
|
+
console.log('Python not found. Installing...');
|
|
35
|
+
return this.installPython();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Auto-detect Python in common locations
|
|
40
|
+
*/
|
|
41
|
+
private async autoDetectPython(): Promise<string | null> {
|
|
42
|
+
const candidates = this.getPythonCandidates();
|
|
43
|
+
|
|
44
|
+
for (const cmd of candidates) {
|
|
45
|
+
if (await this.isValidPython(cmd)) {
|
|
46
|
+
return cmd;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get list of Python commands to try
|
|
55
|
+
*/
|
|
56
|
+
private getPythonCandidates(): string[] {
|
|
57
|
+
const candidates = [
|
|
58
|
+
'python3.12',
|
|
59
|
+
'python3.11',
|
|
60
|
+
'python3.10',
|
|
61
|
+
'python3',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
if (process.platform === 'win32') {
|
|
65
|
+
// Windows-specific paths
|
|
66
|
+
const localAppData = process.env.LOCALAPPDATA || '';
|
|
67
|
+
const programFiles = process.env.ProgramFiles || '';
|
|
68
|
+
|
|
69
|
+
candidates.push(
|
|
70
|
+
join(localAppData, 'Programs', 'Python', 'Python312', 'python.exe'),
|
|
71
|
+
join(localAppData, 'Programs', 'Python', 'Python311', 'python.exe'),
|
|
72
|
+
join(localAppData, 'Programs', 'Python', 'Python310', 'python.exe'),
|
|
73
|
+
join(programFiles, 'Python312', 'python.exe'),
|
|
74
|
+
join(programFiles, 'Python311', 'python.exe'),
|
|
75
|
+
join(programFiles, 'Python310', 'python.exe'),
|
|
76
|
+
'python.exe',
|
|
77
|
+
'python'
|
|
78
|
+
);
|
|
79
|
+
} else {
|
|
80
|
+
// Unix-like paths
|
|
81
|
+
candidates.push(
|
|
82
|
+
'/usr/bin/python3.12',
|
|
83
|
+
'/usr/bin/python3.11',
|
|
84
|
+
'/usr/bin/python3.10',
|
|
85
|
+
'/usr/bin/python3',
|
|
86
|
+
'/usr/local/bin/python3.12',
|
|
87
|
+
'/usr/local/bin/python3.11',
|
|
88
|
+
'/usr/local/bin/python3.10',
|
|
89
|
+
'/usr/local/bin/python3',
|
|
90
|
+
'/opt/homebrew/bin/python3.12',
|
|
91
|
+
'/opt/homebrew/bin/python3.11',
|
|
92
|
+
'/opt/homebrew/bin/python3.10',
|
|
93
|
+
'/opt/homebrew/bin/python3'
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return candidates;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if Python command is valid and meets version requirements
|
|
102
|
+
*/
|
|
103
|
+
async isValidPython(cmd: string): Promise<boolean> {
|
|
104
|
+
try {
|
|
105
|
+
const { stdout } = await this.execAsync(`${cmd} --version`);
|
|
106
|
+
const version = this.parsePythonVersion(stdout);
|
|
107
|
+
return version >= this.MIN_PYTHON_VERSION;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Parse Python version from version string
|
|
115
|
+
*/
|
|
116
|
+
parsePythonVersion(versionOutput: string): number {
|
|
117
|
+
const match = versionOutput.match(/Python (\d+)\.(\d+)/);
|
|
118
|
+
if (!match) return 0;
|
|
119
|
+
|
|
120
|
+
const major = parseInt(match[1], 10);
|
|
121
|
+
const minor = parseInt(match[2], 10);
|
|
122
|
+
return major + minor / 100;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Execute command and return stdout
|
|
127
|
+
*/
|
|
128
|
+
private execAsync(command: string): Promise<{ stdout: string; stderr: string }> {
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
exec(command, (error, stdout, stderr) => {
|
|
131
|
+
if (error) {
|
|
132
|
+
reject(error);
|
|
133
|
+
} else {
|
|
134
|
+
resolve({ stdout, stderr });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Auto-install Python based on platform
|
|
142
|
+
*/
|
|
143
|
+
private async installPython(): Promise<string> {
|
|
144
|
+
if (process.platform === 'win32') {
|
|
145
|
+
return this.installPythonWindows();
|
|
146
|
+
} else if (process.platform === 'darwin') {
|
|
147
|
+
return this.installPythonMacOS();
|
|
148
|
+
} else {
|
|
149
|
+
return this.installPythonLinux();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Install Python on Windows
|
|
155
|
+
*/
|
|
156
|
+
private async installPythonWindows(): Promise<string> {
|
|
157
|
+
try {
|
|
158
|
+
const installerPath = await this.downloadPythonInstaller();
|
|
159
|
+
await this.runPythonInstaller(installerPath);
|
|
160
|
+
return 'python';
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Failed to install Python on Windows: ${error instanceof Error ? error.message : 'Unknown error'}. ` +
|
|
164
|
+
'Please install Python 3.10+ manually from https://python.org'
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Download Python installer for Windows
|
|
171
|
+
*/
|
|
172
|
+
private async downloadPythonInstaller(): Promise<string> {
|
|
173
|
+
// In production, this would download from python.org
|
|
174
|
+
// For now, we assume the user needs to install manually
|
|
175
|
+
throw new Error('Automatic installation not implemented. Please install Python manually.');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Run Python installer on Windows
|
|
180
|
+
*/
|
|
181
|
+
private async runPythonInstaller(installerPath: string): Promise<void> {
|
|
182
|
+
await this.execAsync(`"${installerPath}" /quiet InstallAllUsers=0 PrependPath=1`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Install Python on macOS using Homebrew
|
|
187
|
+
*/
|
|
188
|
+
private async installPythonMacOS(): Promise<string> {
|
|
189
|
+
// Check if brew is available
|
|
190
|
+
try {
|
|
191
|
+
await this.execAsync('which brew');
|
|
192
|
+
} catch {
|
|
193
|
+
throw new Error(
|
|
194
|
+
'Homebrew not found. Please install Homebrew first: https://brew.sh'
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await this.execAsync('brew install python@3.12');
|
|
200
|
+
return '/usr/local/bin/python3.12';
|
|
201
|
+
} catch (error) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`Failed to install Python via Homebrew: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Install Python on Linux using apt
|
|
210
|
+
*/
|
|
211
|
+
private async installPythonLinux(): Promise<string> {
|
|
212
|
+
// Check if apt is available
|
|
213
|
+
try {
|
|
214
|
+
await this.execAsync('which apt');
|
|
215
|
+
} catch {
|
|
216
|
+
throw new Error(
|
|
217
|
+
'apt not found. Please install Python 3.10+ manually using your package manager.'
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
await this.execAsync('sudo apt-get update && sudo apt-get install -y python3.12 python3.12-venv');
|
|
223
|
+
return '/usr/bin/python3.12';
|
|
224
|
+
} catch (error) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`Failed to install Python via apt: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test helper utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { mkdir, rm } from 'fs/promises';
|
|
7
|
+
|
|
8
|
+
export interface TestConfig {
|
|
9
|
+
cascadeGrpcEndpoint: string;
|
|
10
|
+
cascadePythonPath?: string;
|
|
11
|
+
firestoreProjectId?: string;
|
|
12
|
+
firestoreCredentialsPath?: string;
|
|
13
|
+
headless?: boolean;
|
|
14
|
+
actionTimeoutMs?: number;
|
|
15
|
+
enableA2A?: boolean;
|
|
16
|
+
verbose?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createMockConfig(overrides: Partial<TestConfig> = {}): TestConfig {
|
|
20
|
+
return {
|
|
21
|
+
cascadeGrpcEndpoint: 'localhost:50051',
|
|
22
|
+
headless: false,
|
|
23
|
+
actionTimeoutMs: 8000,
|
|
24
|
+
enableA2A: true,
|
|
25
|
+
verbose: false,
|
|
26
|
+
...overrides
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function withTempDir(fn: (dir: string) => Promise<void>): Promise<void> {
|
|
31
|
+
const tmpDir = join(process.cwd(), '.test-tmp', Date.now().toString());
|
|
32
|
+
await mkdir(tmpDir, { recursive: true });
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await fn(tmpDir);
|
|
36
|
+
} finally {
|
|
37
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function waitForCondition(
|
|
42
|
+
condition: () => boolean,
|
|
43
|
+
timeout = 5000,
|
|
44
|
+
interval = 100
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const start = Date.now();
|
|
48
|
+
const check = () => {
|
|
49
|
+
if (condition()) {
|
|
50
|
+
resolve();
|
|
51
|
+
} else if (Date.now() - start > timeout) {
|
|
52
|
+
reject(new Error(`Timeout waiting for condition after ${timeout}ms`));
|
|
53
|
+
} else {
|
|
54
|
+
setTimeout(check, interval);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
check();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function generateBase64Image(size: number): string {
|
|
62
|
+
// Generate fake base64 image data
|
|
63
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
64
|
+
let result = '';
|
|
65
|
+
for (let i = 0; i < size; i++) {
|
|
66
|
+
result += chars[Math.floor(Math.random() * chars.length)];
|
|
67
|
+
}
|
|
68
|
+
// Pad to multiple of 4 for valid base64
|
|
69
|
+
while (result.length % 4 !== 0) {
|
|
70
|
+
result += '=';
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createMockSpawnImplementation(
|
|
76
|
+
stdoutData: string | string[],
|
|
77
|
+
exitCode = 0
|
|
78
|
+
): () => any {
|
|
79
|
+
return () => {
|
|
80
|
+
const { EventEmitter } = require('events');
|
|
81
|
+
const stdout = new EventEmitter();
|
|
82
|
+
const stderr = new EventEmitter();
|
|
83
|
+
|
|
84
|
+
const proc = Object.assign(new EventEmitter(), {
|
|
85
|
+
stdout,
|
|
86
|
+
stderr,
|
|
87
|
+
stdin: { write: jest.fn() },
|
|
88
|
+
kill: jest.fn()
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Emit stdout data after a brief delay
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
const lines = Array.isArray(stdoutData) ? stdoutData : [stdoutData];
|
|
94
|
+
lines.forEach(line => stdout.emit('data', line + '\n'));
|
|
95
|
+
|
|
96
|
+
setTimeout(() => {
|
|
97
|
+
proc.emit('close', exitCode);
|
|
98
|
+
}, 10);
|
|
99
|
+
}, 10);
|
|
100
|
+
|
|
101
|
+
return proc;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function delay(ms: number): Promise<void> {
|
|
106
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
107
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock implementations for testing
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { EventEmitter } from 'events';
|
|
6
|
+
|
|
7
|
+
export interface MockChildProcess {
|
|
8
|
+
stdout: EventEmitter;
|
|
9
|
+
stderr: EventEmitter;
|
|
10
|
+
stdin: {
|
|
11
|
+
write: jest.Mock;
|
|
12
|
+
};
|
|
13
|
+
on: jest.Mock;
|
|
14
|
+
once: jest.Mock;
|
|
15
|
+
emit: jest.Mock;
|
|
16
|
+
kill: jest.Mock;
|
|
17
|
+
pid: number;
|
|
18
|
+
connected: boolean;
|
|
19
|
+
disconnect: jest.Mock;
|
|
20
|
+
unref: jest.Mock;
|
|
21
|
+
ref: jest.Mock;
|
|
22
|
+
send: jest.Mock;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createMockChildProcess(): MockChildProcess {
|
|
26
|
+
const stdout = new EventEmitter();
|
|
27
|
+
const stderr = new EventEmitter();
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
stdout,
|
|
31
|
+
stderr,
|
|
32
|
+
stdin: {
|
|
33
|
+
write: jest.fn()
|
|
34
|
+
},
|
|
35
|
+
on: jest.fn(),
|
|
36
|
+
once: jest.fn(),
|
|
37
|
+
emit: jest.fn(),
|
|
38
|
+
kill: jest.fn(),
|
|
39
|
+
pid: 12345,
|
|
40
|
+
connected: true,
|
|
41
|
+
disconnect: jest.fn(),
|
|
42
|
+
unref: jest.fn(),
|
|
43
|
+
ref: jest.fn(),
|
|
44
|
+
send: jest.fn()
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class MockCascadeMcpClient {
|
|
49
|
+
private tools = new Map<string, Function>();
|
|
50
|
+
public callTool = jest.fn();
|
|
51
|
+
public listTools = jest.fn();
|
|
52
|
+
public isConnected = jest.fn().mockReturnValue(true);
|
|
53
|
+
|
|
54
|
+
registerMockTool(name: string, handler: Function) {
|
|
55
|
+
this.tools.set(name, handler);
|
|
56
|
+
this.callTool.mockImplementation(async (toolName: string, args: any) => {
|
|
57
|
+
if (toolName === name) {
|
|
58
|
+
return handler(args);
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
simulateError(error: Error) {
|
|
65
|
+
this.callTool.mockRejectedValue(error);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
simulateConnectionError() {
|
|
69
|
+
this.isConnected.mockReturnValue(false);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class MockFirestore {
|
|
74
|
+
private data = new Map<string, any>();
|
|
75
|
+
|
|
76
|
+
setDocument(path: string, data: any) {
|
|
77
|
+
this.data.set(path, data);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
collection(path: string) {
|
|
81
|
+
return {
|
|
82
|
+
get: async () => ({
|
|
83
|
+
docs: Array.from(this.data.entries())
|
|
84
|
+
.filter(([key]) => key.startsWith(path))
|
|
85
|
+
.map(([key, value]) => ({
|
|
86
|
+
id: key.split('/').pop(),
|
|
87
|
+
data: () => value
|
|
88
|
+
}))
|
|
89
|
+
}),
|
|
90
|
+
doc: (id: string) => ({
|
|
91
|
+
get: async () => ({
|
|
92
|
+
exists: this.data.has(`${path}/${id}`),
|
|
93
|
+
data: () => this.data.get(`${path}/${id}`)
|
|
94
|
+
}),
|
|
95
|
+
set: async (data: any) => {
|
|
96
|
+
this.data.set(`${path}/${id}`, data);
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Tools (Agent-to-Agent)
|
|
3
|
+
*
|
|
4
|
+
* 3 tools for calling Cascade agents
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ToolRegistry } from './tool-registry';
|
|
8
|
+
import { CascadeA2AClient } from '../a2a-client';
|
|
9
|
+
import { ToolResponse } from '../types';
|
|
10
|
+
import { errorResponse, formatSuccess } from './response-helpers';
|
|
11
|
+
|
|
12
|
+
export function registerA2ATools(registry: ToolRegistry, a2aClient: CascadeA2AClient): void {
|
|
13
|
+
// 1. cascade_run_explorer
|
|
14
|
+
registry.register({
|
|
15
|
+
name: 'cascade_run_explorer',
|
|
16
|
+
description: 'Launch Cascade Explorer agent to learn an application',
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
app_name: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'Application name to explore'
|
|
23
|
+
},
|
|
24
|
+
instructions: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Specific instructions for exploration'
|
|
27
|
+
},
|
|
28
|
+
max_steps: {
|
|
29
|
+
type: 'integer',
|
|
30
|
+
default: 50,
|
|
31
|
+
description: 'Maximum exploration steps'
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
required: ['app_name']
|
|
35
|
+
},
|
|
36
|
+
handler: async (args): Promise<ToolResponse> => {
|
|
37
|
+
try {
|
|
38
|
+
if (!args.app_name) {
|
|
39
|
+
return errorResponse('app_name is required');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await a2aClient.sendToAgent('explorer', {
|
|
43
|
+
type: 'start_exploration',
|
|
44
|
+
appName: args.app_name,
|
|
45
|
+
instructions: args.instructions || `Learn how to use ${args.app_name}`,
|
|
46
|
+
maxSteps: args.max_steps || 50
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return formatSuccess({
|
|
50
|
+
message: 'Explorer agent started',
|
|
51
|
+
app_name: args.app_name,
|
|
52
|
+
status: 'running'
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return errorResponse(
|
|
56
|
+
error instanceof Error ? error.message : 'Failed to start explorer',
|
|
57
|
+
'Ensure A2A is enabled in configuration (enableA2A: true)'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// 2. cascade_run_worker
|
|
64
|
+
registry.register({
|
|
65
|
+
name: 'cascade_run_worker',
|
|
66
|
+
description: 'Execute a task using Cascade Worker agent',
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
task: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
description: 'Task description to execute'
|
|
73
|
+
},
|
|
74
|
+
skill_id: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
description: 'Optional skill ID to use'
|
|
77
|
+
},
|
|
78
|
+
app_name: {
|
|
79
|
+
type: 'string',
|
|
80
|
+
description: 'Target application name'
|
|
81
|
+
},
|
|
82
|
+
inputs: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
description: 'Optional inputs for the task',
|
|
85
|
+
additionalProperties: true
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
required: ['task']
|
|
89
|
+
},
|
|
90
|
+
handler: async (args): Promise<ToolResponse> => {
|
|
91
|
+
try {
|
|
92
|
+
if (!args.task) {
|
|
93
|
+
return errorResponse('task is required');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await a2aClient.sendToAgent('worker', {
|
|
97
|
+
type: 'execute_task',
|
|
98
|
+
task: args.task,
|
|
99
|
+
skillId: args.skill_id,
|
|
100
|
+
appName: args.app_name,
|
|
101
|
+
inputs: args.inputs || {}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return formatSuccess({
|
|
105
|
+
message: 'Worker agent started',
|
|
106
|
+
task: args.task,
|
|
107
|
+
status: 'running'
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return errorResponse(
|
|
111
|
+
error instanceof Error ? error.message : 'Failed to start worker',
|
|
112
|
+
'Ensure A2A is enabled in configuration (enableA2A: true)'
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// 3. cascade_run_orchestrator
|
|
119
|
+
registry.register({
|
|
120
|
+
name: 'cascade_run_orchestrator',
|
|
121
|
+
description: 'Use Cascade Orchestrator to coordinate multi-step tasks',
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
goal: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
description: 'High-level goal to achieve'
|
|
128
|
+
},
|
|
129
|
+
require_approval: {
|
|
130
|
+
type: 'boolean',
|
|
131
|
+
default: true,
|
|
132
|
+
description: 'Require approval before executing steps'
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
required: ['goal']
|
|
136
|
+
},
|
|
137
|
+
handler: async (args): Promise<ToolResponse> => {
|
|
138
|
+
try {
|
|
139
|
+
if (!args.goal) {
|
|
140
|
+
return errorResponse('goal is required');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await a2aClient.sendToAgent('orchestrator', {
|
|
144
|
+
type: 'coordinate',
|
|
145
|
+
goal: args.goal,
|
|
146
|
+
requireApproval: args.require_approval !== false
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return formatSuccess({
|
|
150
|
+
message: 'Orchestrator started',
|
|
151
|
+
goal: args.goal,
|
|
152
|
+
status: 'running'
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return errorResponse(
|
|
156
|
+
error instanceof Error ? error.message : 'Failed to start orchestrator',
|
|
157
|
+
'Ensure A2A is enabled in configuration (enableA2A: true)'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Tools
|
|
3
|
+
*
|
|
4
|
+
* 2 tools for web API calls and search
|
|
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 registerApiTools(registry: ToolRegistry, client: CascadeMcpClient): void {
|
|
13
|
+
// 1. cascade_web_search
|
|
14
|
+
registry.register({
|
|
15
|
+
name: 'cascade_web_search',
|
|
16
|
+
description: 'Search the web for information',
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
query: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'Search query'
|
|
23
|
+
},
|
|
24
|
+
top_k: {
|
|
25
|
+
type: 'integer',
|
|
26
|
+
default: 5,
|
|
27
|
+
description: 'Number of results to return'
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
required: ['query']
|
|
31
|
+
},
|
|
32
|
+
handler: async (args): Promise<ToolResponse> => {
|
|
33
|
+
try {
|
|
34
|
+
if (!args.query) {
|
|
35
|
+
return errorResponse('query is required');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const result = await client.callTool('web_search', {
|
|
39
|
+
query: args.query,
|
|
40
|
+
top_k: args.top_k || 5
|
|
41
|
+
});
|
|
42
|
+
return formatSuccess(result);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return errorResponse(
|
|
45
|
+
error instanceof Error ? error.message : 'Failed to search web',
|
|
46
|
+
'Check your network connection and API key if required'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// 2. cascade_call_http_api
|
|
53
|
+
registry.register({
|
|
54
|
+
name: 'cascade_call_http_api',
|
|
55
|
+
description: 'Execute an HTTP API request',
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
method: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
|
62
|
+
description: 'HTTP method'
|
|
63
|
+
},
|
|
64
|
+
url: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
description: 'Full URL to call'
|
|
67
|
+
},
|
|
68
|
+
headers: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
description: 'Optional HTTP headers',
|
|
71
|
+
additionalProperties: { type: 'string' }
|
|
72
|
+
},
|
|
73
|
+
body: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
description: 'Optional request body (as JSON)'
|
|
76
|
+
},
|
|
77
|
+
timeout: {
|
|
78
|
+
type: 'number',
|
|
79
|
+
default: 30,
|
|
80
|
+
description: 'Request timeout in seconds'
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
required: ['method', 'url']
|
|
84
|
+
},
|
|
85
|
+
handler: async (args): Promise<ToolResponse> => {
|
|
86
|
+
try {
|
|
87
|
+
if (!args.method) {
|
|
88
|
+
return errorResponse('method is required');
|
|
89
|
+
}
|
|
90
|
+
if (!args.url) {
|
|
91
|
+
return errorResponse('url is required');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const result = await client.callTool('call_http_api', {
|
|
95
|
+
method: args.method,
|
|
96
|
+
url: args.url,
|
|
97
|
+
headers: args.headers,
|
|
98
|
+
body: args.body,
|
|
99
|
+
timeout: args.timeout || 30
|
|
100
|
+
});
|
|
101
|
+
return formatSuccess(result);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return errorResponse(
|
|
104
|
+
error instanceof Error ? error.message : 'Failed to call API',
|
|
105
|
+
'Verify the URL is correct and the API is accessible'
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|