@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.
@@ -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,8 @@
1
+ /**
2
+ * Tools Index
3
+ * Central registry for all available tools
4
+ */
5
+ export { BashTool } from './bash.js';
6
+ export type { ToolResult } from './bash.js';
7
+ export { WebTools } from './web.js';
8
+ export { WorkerTools } from './worker.js';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Tools Index
3
+ * Central registry for all available tools
4
+ */
5
+ export { BashTool } from './bash.js';
6
+ export { WebTools } from './web.js';
7
+ export { WorkerTools } from './worker.js';
@@ -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(/&nbsp;/g, ' ')
124
+ .replace(/&amp;/g, '&')
125
+ .replace(/&lt;/g, '<')
126
+ .replace(/&gt;/g, '>')
127
+ .replace(/&quot;/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
+ }