@siftd/connect-agent 0.2.0 → 0.2.3
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/agent.js +120 -26
- package/dist/api.d.ts +1 -0
- package/dist/cli.js +3 -1
- package/dist/config.d.ts +12 -0
- package/dist/config.js +25 -4
- package/dist/core/system-indexer.d.ts +65 -0
- package/dist/core/system-indexer.js +354 -0
- package/dist/genesis/index.d.ts +56 -0
- package/dist/genesis/index.js +71 -0
- package/dist/genesis/system-knowledge.json +62 -0
- package/dist/genesis/tool-patterns.json +88 -0
- package/dist/heartbeat.d.ts +32 -0
- package/dist/heartbeat.js +166 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/orchestrator.d.ts +39 -2
- package/dist/orchestrator.js +547 -78
- package/dist/tools/bash.d.ts +18 -0
- package/dist/tools/bash.js +85 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +7 -0
- package/dist/tools/web.d.ts +38 -0
- package/dist/tools/web.js +284 -0
- package/dist/tools/worker.d.ts +36 -0
- package/dist/tools/worker.js +149 -0
- package/dist/websocket.d.ts +73 -0
- package/dist/websocket.js +243 -0
- package/dist/workers/manager.d.ts +62 -0
- package/dist/workers/manager.js +270 -0
- package/dist/workers/types.d.ts +31 -0
- package/dist/workers/types.js +10 -0
- package/package.json +5 -3
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash Tool
|
|
3
|
+
* Execute shell commands with safety controls
|
|
4
|
+
*/
|
|
5
|
+
export interface ToolResult {
|
|
6
|
+
success: boolean;
|
|
7
|
+
output: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class BashTool {
|
|
11
|
+
private workspaceDir;
|
|
12
|
+
private shellInit;
|
|
13
|
+
constructor(workspaceDir: string);
|
|
14
|
+
/**
|
|
15
|
+
* Execute a bash command
|
|
16
|
+
*/
|
|
17
|
+
execute(command: string, timeout?: number): Promise<ToolResult>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash Tool
|
|
3
|
+
* Execute shell commands with safety controls
|
|
4
|
+
*/
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
const MAX_OUTPUT_LENGTH = 10000;
|
|
9
|
+
const DEFAULT_TIMEOUT = 30000;
|
|
10
|
+
const MAX_TIMEOUT = 120000;
|
|
11
|
+
export class BashTool {
|
|
12
|
+
workspaceDir;
|
|
13
|
+
shellInit;
|
|
14
|
+
constructor(workspaceDir) {
|
|
15
|
+
this.workspaceDir = workspaceDir;
|
|
16
|
+
// Shell initialization for proper environment
|
|
17
|
+
this.shellInit = 'source ~/.zshrc 2>/dev/null || source ~/.bashrc 2>/dev/null || true; ';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Execute a bash command
|
|
21
|
+
*/
|
|
22
|
+
async execute(command, timeout) {
|
|
23
|
+
// Clamp timeout
|
|
24
|
+
const effectiveTimeout = Math.min(Math.max(timeout || DEFAULT_TIMEOUT, 1000), MAX_TIMEOUT);
|
|
25
|
+
try {
|
|
26
|
+
const effectiveCommand = `${this.shellInit}${command}`;
|
|
27
|
+
const { stdout, stderr } = await execAsync(effectiveCommand, {
|
|
28
|
+
cwd: this.workspaceDir,
|
|
29
|
+
timeout: effectiveTimeout,
|
|
30
|
+
maxBuffer: 1024 * 1024 * 10, // 10MB
|
|
31
|
+
shell: '/bin/zsh',
|
|
32
|
+
env: { ...process.env }
|
|
33
|
+
});
|
|
34
|
+
let output = '';
|
|
35
|
+
if (stdout) {
|
|
36
|
+
output += stdout;
|
|
37
|
+
}
|
|
38
|
+
if (stderr) {
|
|
39
|
+
output += (output ? '\n\n' : '') + `[stderr]\n${stderr}`;
|
|
40
|
+
}
|
|
41
|
+
if (!output) {
|
|
42
|
+
output = '(command completed with no output)';
|
|
43
|
+
}
|
|
44
|
+
// Truncate long output
|
|
45
|
+
if (output.length > MAX_OUTPUT_LENGTH) {
|
|
46
|
+
output = output.slice(0, MAX_OUTPUT_LENGTH) +
|
|
47
|
+
`\n\n[... truncated, ${output.length - MAX_OUTPUT_LENGTH} more characters]`;
|
|
48
|
+
}
|
|
49
|
+
return { success: true, output };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error instanceof Error) {
|
|
53
|
+
// Handle timeout
|
|
54
|
+
if (error.message.includes('TIMEOUT')) {
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
output: '',
|
|
58
|
+
error: `Command timed out after ${effectiveTimeout}ms`
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// Handle exec errors (non-zero exit codes)
|
|
62
|
+
const execError = error;
|
|
63
|
+
let output = '';
|
|
64
|
+
if (execError.stdout)
|
|
65
|
+
output += execError.stdout;
|
|
66
|
+
if (execError.stderr)
|
|
67
|
+
output += (output ? '\n\n' : '') + `[stderr]\n${execError.stderr}`;
|
|
68
|
+
// Truncate
|
|
69
|
+
if (output.length > MAX_OUTPUT_LENGTH) {
|
|
70
|
+
output = output.slice(0, MAX_OUTPUT_LENGTH) + `\n\n[... truncated]`;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
output,
|
|
75
|
+
error: `Exit code ${execError.code || 'unknown'}: ${error.message.slice(0, 200)}`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
success: false,
|
|
80
|
+
output: '',
|
|
81
|
+
error: String(error)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Access Tools
|
|
3
|
+
* Fetch URLs and search the web
|
|
4
|
+
*/
|
|
5
|
+
import type { ToolResult } from './bash.js';
|
|
6
|
+
interface FetchOptions {
|
|
7
|
+
selector?: string;
|
|
8
|
+
format?: 'text' | 'html' | 'json';
|
|
9
|
+
timeout?: number;
|
|
10
|
+
maxSize?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class WebTools {
|
|
13
|
+
private maxResponseSize;
|
|
14
|
+
private defaultTimeout;
|
|
15
|
+
constructor(options?: {
|
|
16
|
+
maxResponseSize?: number;
|
|
17
|
+
defaultTimeout?: number;
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Check rate limit
|
|
21
|
+
*/
|
|
22
|
+
private checkRateLimit;
|
|
23
|
+
/**
|
|
24
|
+
* Record a request for rate limiting
|
|
25
|
+
*/
|
|
26
|
+
private recordRequest;
|
|
27
|
+
/**
|
|
28
|
+
* Fetch and parse a URL
|
|
29
|
+
*/
|
|
30
|
+
fetchUrl(url: string, options?: FetchOptions): Promise<ToolResult>;
|
|
31
|
+
/**
|
|
32
|
+
* Search the web using DuckDuckGo HTML (no API key needed)
|
|
33
|
+
*/
|
|
34
|
+
webSearch(query: string, options?: {
|
|
35
|
+
numResults?: number;
|
|
36
|
+
}): Promise<ToolResult>;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Access Tools
|
|
3
|
+
* Fetch URLs and search the web
|
|
4
|
+
*/
|
|
5
|
+
// Rate limiting
|
|
6
|
+
const requestTimes = [];
|
|
7
|
+
const RATE_LIMIT_WINDOW = 60000; // 1 minute
|
|
8
|
+
const MAX_REQUESTS = 10;
|
|
9
|
+
export class WebTools {
|
|
10
|
+
maxResponseSize;
|
|
11
|
+
defaultTimeout;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.maxResponseSize = options?.maxResponseSize || 1024 * 1024; // 1MB
|
|
14
|
+
this.defaultTimeout = options?.defaultTimeout || 10000; // 10s
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Check rate limit
|
|
18
|
+
*/
|
|
19
|
+
checkRateLimit() {
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
// Remove old requests
|
|
22
|
+
while (requestTimes.length > 0 && requestTimes[0] < now - RATE_LIMIT_WINDOW) {
|
|
23
|
+
requestTimes.shift();
|
|
24
|
+
}
|
|
25
|
+
return requestTimes.length < MAX_REQUESTS;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Record a request for rate limiting
|
|
29
|
+
*/
|
|
30
|
+
recordRequest() {
|
|
31
|
+
requestTimes.push(Date.now());
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Fetch and parse a URL
|
|
35
|
+
*/
|
|
36
|
+
async fetchUrl(url, options = {}) {
|
|
37
|
+
// Validate URL
|
|
38
|
+
let parsedUrl;
|
|
39
|
+
try {
|
|
40
|
+
parsedUrl = new URL(url);
|
|
41
|
+
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
output: '',
|
|
45
|
+
error: 'Only HTTP and HTTPS URLs are supported'
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
output: '',
|
|
53
|
+
error: 'Invalid URL format'
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Check rate limit
|
|
57
|
+
if (!this.checkRateLimit()) {
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
output: '',
|
|
61
|
+
error: `Rate limit exceeded. Max ${MAX_REQUESTS} requests per minute.`
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const timeout = Math.min(options.timeout || this.defaultTimeout, 30000);
|
|
65
|
+
const format = options.format || 'text';
|
|
66
|
+
try {
|
|
67
|
+
this.recordRequest();
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
70
|
+
const response = await fetch(url, {
|
|
71
|
+
signal: controller.signal,
|
|
72
|
+
headers: {
|
|
73
|
+
'User-Agent': 'Mozilla/5.0 (compatible; ConnectAgent/1.0)',
|
|
74
|
+
'Accept': format === 'json' ? 'application/json' : 'text/html,text/plain,*/*'
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
clearTimeout(timeoutId);
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
output: '',
|
|
82
|
+
error: `HTTP ${response.status}: ${response.statusText}`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// Check content length
|
|
86
|
+
const contentLength = response.headers.get('content-length');
|
|
87
|
+
if (contentLength && parseInt(contentLength) > this.maxResponseSize) {
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
output: '',
|
|
91
|
+
error: `Response too large: ${contentLength} bytes (max: ${this.maxResponseSize})`
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// Get content type
|
|
95
|
+
const contentType = response.headers.get('content-type') || '';
|
|
96
|
+
// Handle JSON
|
|
97
|
+
if (format === 'json' || contentType.includes('application/json')) {
|
|
98
|
+
const json = await response.json();
|
|
99
|
+
return {
|
|
100
|
+
success: true,
|
|
101
|
+
output: JSON.stringify(json, null, 2)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// Get text content
|
|
105
|
+
const text = await response.text();
|
|
106
|
+
// Check size after fetch (for streaming responses)
|
|
107
|
+
if (text.length > this.maxResponseSize) {
|
|
108
|
+
return {
|
|
109
|
+
success: true,
|
|
110
|
+
output: text.slice(0, this.maxResponseSize) + '\n\n[Content truncated...]'
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Parse HTML if needed - basic extraction without cheerio dependency
|
|
114
|
+
if (contentType.includes('text/html')) {
|
|
115
|
+
// Extract text content from HTML
|
|
116
|
+
let mainContent = text
|
|
117
|
+
// Remove script and style tags
|
|
118
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
119
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
120
|
+
// Remove HTML tags
|
|
121
|
+
.replace(/<[^>]+>/g, ' ')
|
|
122
|
+
// Decode HTML entities
|
|
123
|
+
.replace(/ /g, ' ')
|
|
124
|
+
.replace(/&/g, '&')
|
|
125
|
+
.replace(/</g, '<')
|
|
126
|
+
.replace(/>/g, '>')
|
|
127
|
+
.replace(/"/g, '"')
|
|
128
|
+
// Clean up whitespace
|
|
129
|
+
.replace(/\s+/g, ' ')
|
|
130
|
+
.trim();
|
|
131
|
+
// Extract title
|
|
132
|
+
const titleMatch = text.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
133
|
+
const title = titleMatch ? titleMatch[1].trim() : '';
|
|
134
|
+
let output = '';
|
|
135
|
+
if (title) {
|
|
136
|
+
output += `Title: ${title}\n\n`;
|
|
137
|
+
}
|
|
138
|
+
output += mainContent;
|
|
139
|
+
// Truncate if too long
|
|
140
|
+
if (output.length > 10000) {
|
|
141
|
+
output = output.slice(0, 10000) + '\n\n[Content truncated...]';
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
success: true,
|
|
145
|
+
output
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// Plain text
|
|
149
|
+
return {
|
|
150
|
+
success: true,
|
|
151
|
+
output: text
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (error instanceof Error) {
|
|
156
|
+
if (error.name === 'AbortError') {
|
|
157
|
+
return {
|
|
158
|
+
success: false,
|
|
159
|
+
output: '',
|
|
160
|
+
error: `Request timed out after ${timeout}ms`
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
success: false,
|
|
165
|
+
output: '',
|
|
166
|
+
error: error.message
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
success: false,
|
|
171
|
+
output: '',
|
|
172
|
+
error: String(error)
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Search the web using DuckDuckGo HTML (no API key needed)
|
|
178
|
+
*/
|
|
179
|
+
async webSearch(query, options) {
|
|
180
|
+
const numResults = Math.min(options?.numResults || 5, 10);
|
|
181
|
+
// Check rate limit
|
|
182
|
+
if (!this.checkRateLimit()) {
|
|
183
|
+
return {
|
|
184
|
+
success: false,
|
|
185
|
+
output: '',
|
|
186
|
+
error: `Rate limit exceeded. Max ${MAX_REQUESTS} requests per minute.`
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
this.recordRequest();
|
|
191
|
+
// Use DuckDuckGo HTML
|
|
192
|
+
const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
193
|
+
const controller = new AbortController();
|
|
194
|
+
const timeoutId = setTimeout(() => controller.abort(), 15000);
|
|
195
|
+
const response = await fetch(searchUrl, {
|
|
196
|
+
signal: controller.signal,
|
|
197
|
+
headers: {
|
|
198
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
199
|
+
'Accept': 'text/html'
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
clearTimeout(timeoutId);
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
return {
|
|
205
|
+
success: false,
|
|
206
|
+
output: '',
|
|
207
|
+
error: `Search failed: HTTP ${response.status}`
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const html = await response.text();
|
|
211
|
+
// Parse results with regex (no cheerio)
|
|
212
|
+
const results = [];
|
|
213
|
+
// Match result divs - DuckDuckGo structure
|
|
214
|
+
const resultPattern = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>[\s\S]*?<a[^>]*class="result__snippet"[^>]*>([^<]*)<\/a>/gi;
|
|
215
|
+
let match;
|
|
216
|
+
while ((match = resultPattern.exec(html)) !== null && results.length < numResults) {
|
|
217
|
+
let url = match[1];
|
|
218
|
+
const title = match[2].trim();
|
|
219
|
+
const snippet = match[3].trim();
|
|
220
|
+
// DuckDuckGo uses redirect URLs, extract the actual URL
|
|
221
|
+
if (url.includes('uddg=')) {
|
|
222
|
+
const uddgMatch = url.match(/uddg=([^&]+)/);
|
|
223
|
+
if (uddgMatch) {
|
|
224
|
+
url = decodeURIComponent(uddgMatch[1]);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (title && url) {
|
|
228
|
+
results.push({ title, url, snippet });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Fallback: simpler pattern
|
|
232
|
+
if (results.length === 0) {
|
|
233
|
+
const simplePattern = /<a[^>]*href="(https?:\/\/[^"]+)"[^>]*>([^<]+)<\/a>/gi;
|
|
234
|
+
while ((match = simplePattern.exec(html)) !== null && results.length < numResults) {
|
|
235
|
+
const url = match[1];
|
|
236
|
+
const title = match[2].trim();
|
|
237
|
+
if (title && url && !url.includes('duckduckgo.com')) {
|
|
238
|
+
results.push({ title, url, snippet: '' });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (results.length === 0) {
|
|
243
|
+
return {
|
|
244
|
+
success: true,
|
|
245
|
+
output: 'No results found for this query.'
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
let output = `Search results for: "${query}"\n\n`;
|
|
249
|
+
results.forEach((result, i) => {
|
|
250
|
+
output += `${i + 1}. ${result.title}\n`;
|
|
251
|
+
output += ` URL: ${result.url}\n`;
|
|
252
|
+
if (result.snippet) {
|
|
253
|
+
output += ` ${result.snippet}\n`;
|
|
254
|
+
}
|
|
255
|
+
output += '\n';
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
success: true,
|
|
259
|
+
output
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
if (error instanceof Error) {
|
|
264
|
+
if (error.name === 'AbortError') {
|
|
265
|
+
return {
|
|
266
|
+
success: false,
|
|
267
|
+
output: '',
|
|
268
|
+
error: 'Search timed out'
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
success: false,
|
|
273
|
+
output: '',
|
|
274
|
+
error: error.message
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
success: false,
|
|
279
|
+
output: '',
|
|
280
|
+
error: String(error)
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Tools
|
|
3
|
+
* Tools for spawning and managing Claude Code workers
|
|
4
|
+
*/
|
|
5
|
+
import type { ToolResult } from './bash.js';
|
|
6
|
+
export declare class WorkerTools {
|
|
7
|
+
private manager;
|
|
8
|
+
constructor(workspaceDir: string);
|
|
9
|
+
/**
|
|
10
|
+
* Spawn a new Claude Code worker
|
|
11
|
+
*/
|
|
12
|
+
spawnWorker(task: string, options?: {
|
|
13
|
+
timeout?: number;
|
|
14
|
+
priority?: 'low' | 'normal' | 'high' | 'critical';
|
|
15
|
+
}): Promise<ToolResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Check worker status
|
|
18
|
+
*/
|
|
19
|
+
checkWorker(jobId: string): Promise<ToolResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Wait for worker to complete
|
|
22
|
+
*/
|
|
23
|
+
waitWorker(jobId: string, maxWait?: number): Promise<ToolResult>;
|
|
24
|
+
/**
|
|
25
|
+
* List all workers
|
|
26
|
+
*/
|
|
27
|
+
listWorkers(status?: string): Promise<ToolResult>;
|
|
28
|
+
/**
|
|
29
|
+
* Cancel a running worker
|
|
30
|
+
*/
|
|
31
|
+
cancelWorker(jobId: string): Promise<ToolResult>;
|
|
32
|
+
/**
|
|
33
|
+
* Clean up old jobs
|
|
34
|
+
*/
|
|
35
|
+
cleanupWorkers(): Promise<ToolResult>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Tools
|
|
3
|
+
* Tools for spawning and managing Claude Code workers
|
|
4
|
+
*/
|
|
5
|
+
import { WorkerManager } from '../workers/manager.js';
|
|
6
|
+
export class WorkerTools {
|
|
7
|
+
manager;
|
|
8
|
+
constructor(workspaceDir) {
|
|
9
|
+
this.manager = new WorkerManager(workspaceDir);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Spawn a new Claude Code worker
|
|
13
|
+
*/
|
|
14
|
+
async spawnWorker(task, options) {
|
|
15
|
+
try {
|
|
16
|
+
const jobId = await this.manager.spawn(task, {
|
|
17
|
+
timeout: options?.timeout,
|
|
18
|
+
priority: options?.priority
|
|
19
|
+
});
|
|
20
|
+
const job = this.manager.get(jobId);
|
|
21
|
+
return {
|
|
22
|
+
success: true,
|
|
23
|
+
output: `Worker spawned successfully.\nJob ID: ${jobId}\nStatus: ${job?.status || 'pending'}\nTask: ${task}\n\nUse check_worker("${jobId}") to check status, or wait_worker("${jobId}") to wait for completion.`
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return {
|
|
28
|
+
success: false,
|
|
29
|
+
output: '',
|
|
30
|
+
error: error instanceof Error ? error.message : String(error)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check worker status
|
|
36
|
+
*/
|
|
37
|
+
async checkWorker(jobId) {
|
|
38
|
+
const job = this.manager.get(jobId);
|
|
39
|
+
if (!job) {
|
|
40
|
+
return {
|
|
41
|
+
success: false,
|
|
42
|
+
output: '',
|
|
43
|
+
error: `Job ${jobId} not found`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
let output = `Job: ${job.id}\nStatus: ${job.status}\nTask: ${job.task}\nCreated: ${job.created}`;
|
|
47
|
+
if (job.started) {
|
|
48
|
+
output += `\nStarted: ${job.started}`;
|
|
49
|
+
}
|
|
50
|
+
if (job.completed) {
|
|
51
|
+
output += `\nCompleted: ${job.completed}`;
|
|
52
|
+
}
|
|
53
|
+
if (job.result) {
|
|
54
|
+
output += `\n\nResult:\n${job.result}`;
|
|
55
|
+
}
|
|
56
|
+
if (job.error) {
|
|
57
|
+
output += `\n\nError: ${job.error}`;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
success: true,
|
|
61
|
+
output
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Wait for worker to complete
|
|
66
|
+
*/
|
|
67
|
+
async waitWorker(jobId, maxWait) {
|
|
68
|
+
try {
|
|
69
|
+
const job = await this.manager.wait(jobId, maxWait);
|
|
70
|
+
let output = `Job ${job.id} ${job.status}.\n`;
|
|
71
|
+
if (job.result) {
|
|
72
|
+
output += `\nResult:\n${job.result}`;
|
|
73
|
+
}
|
|
74
|
+
if (job.error) {
|
|
75
|
+
output += `\nError: ${job.error}`;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
success: job.status === 'completed',
|
|
79
|
+
output,
|
|
80
|
+
error: job.status !== 'completed' ? `Job ${job.status}` : undefined
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
output: '',
|
|
87
|
+
error: error instanceof Error ? error.message : String(error)
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* List all workers
|
|
93
|
+
*/
|
|
94
|
+
async listWorkers(status) {
|
|
95
|
+
const jobs = this.manager.list(status ? { status } : undefined);
|
|
96
|
+
const summary = this.manager.summary();
|
|
97
|
+
if (jobs.length === 0) {
|
|
98
|
+
return {
|
|
99
|
+
success: true,
|
|
100
|
+
output: 'No jobs found.'
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
let output = `Jobs Summary: ${summary.total} total (${summary.running} running, ${summary.completed} completed, ${summary.failed} failed)\n\n`;
|
|
104
|
+
for (const job of jobs.slice(0, 20)) { // Limit to 20 most recent
|
|
105
|
+
output += `[${job.status.toUpperCase()}] ${job.id}\n`;
|
|
106
|
+
output += ` Task: ${job.task.slice(0, 80)}${job.task.length > 80 ? '...' : ''}\n`;
|
|
107
|
+
output += ` Created: ${job.created}\n`;
|
|
108
|
+
if (job.result && job.status === 'completed') {
|
|
109
|
+
const preview = job.result.slice(0, 100);
|
|
110
|
+
output += ` Result: ${preview}${job.result.length > 100 ? '...' : ''}\n`;
|
|
111
|
+
}
|
|
112
|
+
output += '\n';
|
|
113
|
+
}
|
|
114
|
+
if (jobs.length > 20) {
|
|
115
|
+
output += `... and ${jobs.length - 20} more jobs`;
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
success: true,
|
|
119
|
+
output
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Cancel a running worker
|
|
124
|
+
*/
|
|
125
|
+
async cancelWorker(jobId) {
|
|
126
|
+
const cancelled = this.manager.cancel(jobId);
|
|
127
|
+
if (cancelled) {
|
|
128
|
+
return {
|
|
129
|
+
success: true,
|
|
130
|
+
output: `Job ${jobId} cancelled successfully.`
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
success: false,
|
|
135
|
+
output: '',
|
|
136
|
+
error: `Could not cancel job ${jobId}. It may not exist or is not running.`
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Clean up old jobs
|
|
141
|
+
*/
|
|
142
|
+
async cleanupWorkers() {
|
|
143
|
+
const cleaned = this.manager.cleanup();
|
|
144
|
+
return {
|
|
145
|
+
success: true,
|
|
146
|
+
output: `Cleaned up ${cleaned} old job(s).`
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|