@gopherhole/sdk 0.1.0 → 0.1.1
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/README.md +140 -73
- package/dist/index.d.mts +499 -0
- package/dist/index.d.ts +472 -40
- package/dist/index.js +442 -197
- package/dist/index.mjs +433 -0
- package/package.json +39 -7
- package/src/index.ts +607 -204
- package/src/types.ts +232 -0
- package/tsconfig.json +9 -6
- package/listen-marketclaw.ts +0 -26
- package/send-from-nova.ts +0 -28
- package/test.ts +0 -86
package/src/index.ts
CHANGED
|
@@ -1,277 +1,680 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { EventEmitter } from 'eventemitter3';
|
|
2
|
+
|
|
3
|
+
// Re-export types
|
|
4
|
+
export * from './types';
|
|
3
5
|
|
|
4
6
|
export interface GopherHoleOptions {
|
|
7
|
+
/** API key (starts with gph_) */
|
|
8
|
+
apiKey: string;
|
|
9
|
+
/** Hub URL (defaults to production) */
|
|
5
10
|
hubUrl?: string;
|
|
6
|
-
|
|
11
|
+
/** Agent card to register on connect */
|
|
12
|
+
agentCard?: AgentCardConfig;
|
|
13
|
+
/** Auto-reconnect on disconnect */
|
|
14
|
+
autoReconnect?: boolean;
|
|
15
|
+
/** Reconnect delay in ms */
|
|
7
16
|
reconnectDelay?: number;
|
|
17
|
+
/** Max reconnect attempts */
|
|
18
|
+
maxReconnectAttempts?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Agent card configuration for registration */
|
|
22
|
+
export interface AgentCardConfig {
|
|
23
|
+
name: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
url?: string;
|
|
26
|
+
version?: string;
|
|
27
|
+
skills?: AgentSkillConfig[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Skill configuration */
|
|
31
|
+
export interface AgentSkillConfig {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
tags?: string[];
|
|
36
|
+
examples?: string[];
|
|
37
|
+
inputModes?: string[];
|
|
38
|
+
outputModes?: string[];
|
|
8
39
|
}
|
|
9
40
|
|
|
10
41
|
export interface Message {
|
|
11
42
|
from: string;
|
|
12
|
-
|
|
43
|
+
taskId?: string;
|
|
13
44
|
payload: MessagePayload;
|
|
14
45
|
timestamp: number;
|
|
15
46
|
}
|
|
16
47
|
|
|
17
48
|
export interface MessagePayload {
|
|
49
|
+
role: 'user' | 'agent';
|
|
18
50
|
parts: MessagePart[];
|
|
19
|
-
contextId?: string;
|
|
20
51
|
}
|
|
21
52
|
|
|
22
53
|
export interface MessagePart {
|
|
23
|
-
kind: 'text' | '
|
|
54
|
+
kind: 'text' | 'file' | 'data';
|
|
24
55
|
text?: string;
|
|
25
|
-
data?: unknown;
|
|
26
56
|
mimeType?: string;
|
|
57
|
+
data?: string;
|
|
58
|
+
uri?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface Task {
|
|
62
|
+
id: string;
|
|
63
|
+
contextId: string;
|
|
64
|
+
status: TaskStatus;
|
|
65
|
+
history?: MessagePayload[];
|
|
66
|
+
artifacts?: Artifact[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface TaskStatus {
|
|
70
|
+
state: 'submitted' | 'working' | 'input-required' | 'completed' | 'failed' | 'canceled' | 'rejected';
|
|
71
|
+
timestamp: string;
|
|
72
|
+
message?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface Artifact {
|
|
76
|
+
name: string;
|
|
77
|
+
mimeType: string;
|
|
78
|
+
data?: string;
|
|
27
79
|
uri?: string;
|
|
28
80
|
}
|
|
29
81
|
|
|
30
|
-
interface
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
error?: string;
|
|
38
|
-
timestamp?: number;
|
|
82
|
+
export interface SendOptions {
|
|
83
|
+
/** Existing context/conversation ID */
|
|
84
|
+
contextId?: string;
|
|
85
|
+
/** Push notification URL */
|
|
86
|
+
pushNotificationUrl?: string;
|
|
87
|
+
/** History length to include */
|
|
88
|
+
historyLength?: number;
|
|
39
89
|
}
|
|
40
90
|
|
|
41
|
-
|
|
91
|
+
type EventMap = {
|
|
92
|
+
connect: () => void;
|
|
93
|
+
disconnect: (reason: string) => void;
|
|
94
|
+
error: (error: Error) => void;
|
|
95
|
+
message: (message: Message) => void;
|
|
96
|
+
taskUpdate: (task: Task) => void;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const DEFAULT_HUB_URL = 'wss://gopherhole.helixdata.workers.dev/ws';
|
|
100
|
+
const DEFAULT_API_URL = 'https://gopherhole.helixdata.workers.dev';
|
|
101
|
+
|
|
102
|
+
export class GopherHole extends EventEmitter<EventMap> {
|
|
42
103
|
private apiKey: string;
|
|
43
104
|
private hubUrl: string;
|
|
105
|
+
private apiUrl: string;
|
|
44
106
|
private ws: WebSocket | null = null;
|
|
45
|
-
private
|
|
107
|
+
private autoReconnect: boolean;
|
|
46
108
|
private reconnectDelay: number;
|
|
47
|
-
private
|
|
48
|
-
private
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
109
|
+
private maxReconnectAttempts: number;
|
|
110
|
+
private reconnectAttempts = 0;
|
|
111
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
112
|
+
private pingInterval: ReturnType<typeof setInterval> | null = null;
|
|
113
|
+
private agentId: string | null = null;
|
|
114
|
+
private agentCard: AgentCardConfig | null = null;
|
|
115
|
+
|
|
116
|
+
constructor(apiKeyOrOptions: string | GopherHoleOptions) {
|
|
53
117
|
super();
|
|
54
|
-
this.apiKey = apiKey;
|
|
55
|
-
this.hubUrl = options.hubUrl || 'wss://gopherhole.ai/ws';
|
|
56
|
-
this.reconnect = options.reconnect ?? true;
|
|
57
|
-
this.reconnectDelay = options.reconnectDelay ?? 5000;
|
|
58
|
-
}
|
|
59
118
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
this.authenticated = false;
|
|
82
|
-
this.emit('disconnect');
|
|
83
|
-
|
|
84
|
-
if (this.reconnect) {
|
|
85
|
-
console.log(`[GopherHole] Reconnecting in ${this.reconnectDelay}ms...`);
|
|
86
|
-
setTimeout(() => this.connect(), this.reconnectDelay);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
this.ws.on('error', (err) => {
|
|
91
|
-
console.error('[GopherHole] WebSocket error:', err);
|
|
92
|
-
this.emit('error', err);
|
|
93
|
-
if (!this.authenticated) {
|
|
94
|
-
reject(err);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
} catch (err) {
|
|
98
|
-
reject(err);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
119
|
+
const options = typeof apiKeyOrOptions === 'string'
|
|
120
|
+
? { apiKey: apiKeyOrOptions }
|
|
121
|
+
: apiKeyOrOptions;
|
|
122
|
+
|
|
123
|
+
this.apiKey = options.apiKey;
|
|
124
|
+
this.hubUrl = options.hubUrl || DEFAULT_HUB_URL;
|
|
125
|
+
this.apiUrl = this.hubUrl.replace('/ws', '').replace('wss://', 'https://').replace('ws://', 'http://');
|
|
126
|
+
this.agentCard = options.agentCard || null;
|
|
127
|
+
this.autoReconnect = options.autoReconnect ?? true;
|
|
128
|
+
this.reconnectDelay = options.reconnectDelay ?? 1000;
|
|
129
|
+
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Update agent card (sends to hub if connected)
|
|
134
|
+
*/
|
|
135
|
+
async updateCard(card: AgentCardConfig): Promise<void> {
|
|
136
|
+
this.agentCard = card;
|
|
137
|
+
if (this.ws?.readyState === 1) {
|
|
138
|
+
this.ws.send(JSON.stringify({ type: 'update_card', agentCard: card }));
|
|
139
|
+
}
|
|
101
140
|
}
|
|
102
141
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
this.pendingMessages.delete(msg.id!);
|
|
142
|
+
/**
|
|
143
|
+
* Connect to the GopherHole hub via WebSocket
|
|
144
|
+
*/
|
|
145
|
+
async connect(): Promise<void> {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
// Browser or Node WebSocket
|
|
148
|
+
const WS = typeof WebSocket !== 'undefined' ? WebSocket : require('ws');
|
|
149
|
+
|
|
150
|
+
const ws = new WS(this.hubUrl, {
|
|
151
|
+
headers: {
|
|
152
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
153
|
+
},
|
|
154
|
+
}) as WebSocket;
|
|
155
|
+
|
|
156
|
+
this.ws = ws;
|
|
157
|
+
|
|
158
|
+
ws.onopen = () => {
|
|
159
|
+
this.reconnectAttempts = 0;
|
|
160
|
+
this.startPing();
|
|
161
|
+
this.emit('connect');
|
|
162
|
+
resolve();
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
ws.onclose = (event: CloseEvent) => {
|
|
166
|
+
this.stopPing();
|
|
167
|
+
const reason = event.reason || 'Connection closed';
|
|
168
|
+
this.emit('disconnect', reason);
|
|
169
|
+
|
|
170
|
+
if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
171
|
+
this.scheduleReconnect();
|
|
134
172
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
ws.onerror = () => {
|
|
176
|
+
const error = new Error('WebSocket error');
|
|
177
|
+
this.emit('error', error);
|
|
178
|
+
reject(error);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
ws.onmessage = (event: MessageEvent) => {
|
|
182
|
+
try {
|
|
183
|
+
const data = JSON.parse(event.data.toString());
|
|
184
|
+
this.handleMessage(data);
|
|
185
|
+
} catch {
|
|
186
|
+
this.emit('error', new Error('Failed to parse message'));
|
|
143
187
|
}
|
|
144
|
-
|
|
145
|
-
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
}
|
|
146
191
|
|
|
147
|
-
|
|
148
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Disconnect from the hub
|
|
194
|
+
*/
|
|
195
|
+
disconnect(): void {
|
|
196
|
+
this.autoReconnect = false;
|
|
197
|
+
this.stopPing();
|
|
198
|
+
if (this.reconnectTimer) {
|
|
199
|
+
clearTimeout(this.reconnectTimer);
|
|
200
|
+
this.reconnectTimer = null;
|
|
201
|
+
}
|
|
202
|
+
if (this.ws) {
|
|
203
|
+
this.ws.close();
|
|
204
|
+
this.ws = null;
|
|
149
205
|
}
|
|
150
206
|
}
|
|
151
207
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Send a message to another agent
|
|
210
|
+
*/
|
|
211
|
+
async send(toAgentId: string, payload: MessagePayload, options?: SendOptions): Promise<Task> {
|
|
212
|
+
const response = await this.rpc('message/send', {
|
|
213
|
+
message: payload,
|
|
214
|
+
configuration: {
|
|
215
|
+
agentId: toAgentId,
|
|
216
|
+
...options,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
156
219
|
|
|
157
|
-
|
|
220
|
+
return response as Task;
|
|
221
|
+
}
|
|
158
222
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Send a text message to another agent
|
|
225
|
+
*/
|
|
226
|
+
async sendText(toAgentId: string, text: string, options?: SendOptions): Promise<Task> {
|
|
227
|
+
return this.send(toAgentId, {
|
|
228
|
+
role: 'agent',
|
|
229
|
+
parts: [{ kind: 'text', text }],
|
|
230
|
+
}, options);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get a task by ID
|
|
235
|
+
*/
|
|
236
|
+
async getTask(taskId: string, historyLength?: number): Promise<Task> {
|
|
237
|
+
const response = await this.rpc('tasks/get', {
|
|
238
|
+
id: taskId,
|
|
239
|
+
historyLength,
|
|
240
|
+
});
|
|
241
|
+
return response as Task;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* List tasks
|
|
246
|
+
*/
|
|
247
|
+
async listTasks(options?: {
|
|
248
|
+
contextId?: string;
|
|
249
|
+
pageSize?: number;
|
|
250
|
+
pageToken?: string;
|
|
251
|
+
}): Promise<{ tasks: Task[]; nextPageToken?: string; totalSize: number }> {
|
|
252
|
+
const response = await this.rpc('tasks/list', options || {});
|
|
253
|
+
return response as { tasks: Task[]; nextPageToken?: string; totalSize: number };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Cancel a task
|
|
258
|
+
*/
|
|
259
|
+
async cancelTask(taskId: string): Promise<Task> {
|
|
260
|
+
const response = await this.rpc('tasks/cancel', { id: taskId });
|
|
261
|
+
return response as Task;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Reply to a message/task (sends back to the original caller)
|
|
266
|
+
*/
|
|
267
|
+
async reply(taskId: string, payload: MessagePayload, toAgentId?: string): Promise<Task> {
|
|
268
|
+
// If toAgentId not provided, we need to figure out who to reply to
|
|
269
|
+
// For now, require the caller to provide it or pass through the task context
|
|
270
|
+
if (!toAgentId) {
|
|
271
|
+
// Get task to use same context
|
|
272
|
+
const task = await this.getTask(taskId);
|
|
273
|
+
// Note: The task doesn't expose client_agent_id via API, so we send to context
|
|
274
|
+
// The server should route based on contextId
|
|
275
|
+
const response = await this.rpc('message/send', {
|
|
276
|
+
message: payload,
|
|
277
|
+
configuration: {
|
|
278
|
+
contextId: task.contextId,
|
|
279
|
+
// Server needs to handle replies via context routing
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
return response as Task;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const response = await this.rpc('message/send', {
|
|
286
|
+
message: payload,
|
|
287
|
+
configuration: {
|
|
288
|
+
agentId: toAgentId,
|
|
289
|
+
},
|
|
176
290
|
});
|
|
291
|
+
|
|
292
|
+
return response as Task;
|
|
177
293
|
}
|
|
178
294
|
|
|
179
|
-
|
|
180
|
-
|
|
295
|
+
/**
|
|
296
|
+
* Reply with text
|
|
297
|
+
*/
|
|
298
|
+
async replyText(taskId: string, text: string): Promise<Task> {
|
|
299
|
+
return this.reply(taskId, {
|
|
300
|
+
role: 'agent',
|
|
181
301
|
parts: [{ kind: 'text', text }],
|
|
182
|
-
contextId,
|
|
183
302
|
});
|
|
184
303
|
}
|
|
185
304
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Make a JSON-RPC call to the A2A endpoint
|
|
307
|
+
*/
|
|
308
|
+
private async rpc(method: string, params: Record<string, unknown>): Promise<unknown> {
|
|
309
|
+
const response = await fetch(`${this.apiUrl}/a2a`, {
|
|
310
|
+
method: 'POST',
|
|
311
|
+
headers: {
|
|
312
|
+
'Content-Type': 'application/json',
|
|
313
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
314
|
+
},
|
|
315
|
+
body: JSON.stringify({
|
|
316
|
+
jsonrpc: '2.0',
|
|
317
|
+
method,
|
|
318
|
+
params,
|
|
319
|
+
id: Date.now(),
|
|
320
|
+
}),
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const data = await response.json();
|
|
324
|
+
|
|
325
|
+
if (data.error) {
|
|
326
|
+
throw new Error(data.error.message || 'RPC error');
|
|
191
327
|
}
|
|
192
|
-
}
|
|
193
328
|
|
|
194
|
-
|
|
195
|
-
return this.authenticated && this.ws?.readyState === WebSocket.OPEN;
|
|
329
|
+
return data.result;
|
|
196
330
|
}
|
|
197
331
|
|
|
198
332
|
/**
|
|
199
|
-
*
|
|
200
|
-
* Reads GOPHERHOLE_API_KEY and optionally GOPHERHOLE_HUB_URL
|
|
333
|
+
* Handle incoming WebSocket messages
|
|
201
334
|
*/
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
335
|
+
private handleMessage(data: any): void {
|
|
336
|
+
if (data.type === 'message') {
|
|
337
|
+
this.emit('message', {
|
|
338
|
+
from: data.from,
|
|
339
|
+
taskId: data.taskId,
|
|
340
|
+
payload: data.payload,
|
|
341
|
+
timestamp: data.timestamp || Date.now(),
|
|
342
|
+
});
|
|
343
|
+
} else if (data.type === 'task_update') {
|
|
344
|
+
this.emit('taskUpdate', data.task);
|
|
345
|
+
} else if (data.type === 'pong') {
|
|
346
|
+
// Heartbeat response
|
|
347
|
+
} else if (data.type === 'welcome') {
|
|
348
|
+
this.agentId = data.agentId;
|
|
349
|
+
// Send agent card if configured
|
|
350
|
+
if (this.agentCard && this.ws?.readyState === 1) {
|
|
351
|
+
this.ws.send(JSON.stringify({ type: 'update_card', agentCard: this.agentCard }));
|
|
352
|
+
}
|
|
353
|
+
} else if (data.type === 'card_updated') {
|
|
354
|
+
// Agent card was successfully updated
|
|
355
|
+
} else if (data.type === 'warning') {
|
|
356
|
+
console.warn('GopherHole warning:', data.message);
|
|
206
357
|
}
|
|
207
|
-
return new GopherHole(apiKey, {
|
|
208
|
-
...options,
|
|
209
|
-
hubUrl: process.env.GOPHERHOLE_HUB_URL || 'wss://gopherhole.ai/ws',
|
|
210
|
-
});
|
|
211
358
|
}
|
|
212
359
|
|
|
213
360
|
/**
|
|
214
|
-
*
|
|
361
|
+
* Start ping interval
|
|
362
|
+
*/
|
|
363
|
+
private startPing(): void {
|
|
364
|
+
this.pingInterval = setInterval(() => {
|
|
365
|
+
if (this.ws?.readyState === 1) { // OPEN
|
|
366
|
+
this.ws.send(JSON.stringify({ type: 'ping' }));
|
|
367
|
+
}
|
|
368
|
+
}, 30000);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Stop ping interval
|
|
215
373
|
*/
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
onConnect?: (hub: GopherHole) => void;
|
|
221
|
-
onError?: (err: Error, hub: GopherHole) => void;
|
|
222
|
-
}): { start: () => Promise<void>; stop: () => void; hub: GopherHole } {
|
|
223
|
-
const apiKey = config.apiKey || process.env.GOPHERHOLE_API_KEY;
|
|
224
|
-
if (!apiKey) {
|
|
225
|
-
throw new Error('API key required: pass apiKey or set GOPHERHOLE_API_KEY');
|
|
374
|
+
private stopPing(): void {
|
|
375
|
+
if (this.pingInterval) {
|
|
376
|
+
clearInterval(this.pingInterval);
|
|
377
|
+
this.pingInterval = null;
|
|
226
378
|
}
|
|
379
|
+
}
|
|
227
380
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
381
|
+
/**
|
|
382
|
+
* Schedule reconnection
|
|
383
|
+
*/
|
|
384
|
+
private scheduleReconnect(): void {
|
|
385
|
+
if (this.reconnectTimer) return;
|
|
386
|
+
|
|
387
|
+
this.reconnectAttempts++;
|
|
388
|
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
231
389
|
|
|
232
|
-
|
|
390
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
391
|
+
this.reconnectTimer = null;
|
|
233
392
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
//
|
|
237
|
-
if (response) {
|
|
238
|
-
if (typeof response === 'string') {
|
|
239
|
-
await hub.sendText(msg.from, response);
|
|
240
|
-
} else {
|
|
241
|
-
await hub.send(msg.from, response);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
} catch (err) {
|
|
245
|
-
console.error('[GopherHole Agent] Message handler error:', err);
|
|
246
|
-
if (config.onError) {
|
|
247
|
-
config.onError(err as Error, hub);
|
|
248
|
-
}
|
|
393
|
+
await this.connect();
|
|
394
|
+
} catch {
|
|
395
|
+
// Will retry via onclose handler
|
|
249
396
|
}
|
|
397
|
+
}, delay);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Get connection state
|
|
402
|
+
*/
|
|
403
|
+
get connected(): boolean {
|
|
404
|
+
return this.ws?.readyState === 1;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Get the agent ID (available after connect)
|
|
409
|
+
*/
|
|
410
|
+
get id(): string | null {
|
|
411
|
+
return this.agentId;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ============================================================
|
|
415
|
+
// DISCOVERY METHODS
|
|
416
|
+
// ============================================================
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Discover public agents with comprehensive search
|
|
420
|
+
*/
|
|
421
|
+
async discover(options?: DiscoverOptions): Promise<DiscoverResult> {
|
|
422
|
+
const params = new URLSearchParams();
|
|
423
|
+
|
|
424
|
+
if (options?.query) params.set('q', options.query);
|
|
425
|
+
if (options?.category) params.set('category', options.category);
|
|
426
|
+
if (options?.tag) params.set('tag', options.tag);
|
|
427
|
+
if (options?.skillTag) params.set('skillTag', options.skillTag);
|
|
428
|
+
if (options?.contentMode) params.set('contentMode', options.contentMode);
|
|
429
|
+
if (options?.sort) params.set('sort', options.sort);
|
|
430
|
+
if (options?.limit) params.set('limit', String(options.limit));
|
|
431
|
+
if (options?.offset) params.set('offset', String(options.offset));
|
|
432
|
+
|
|
433
|
+
// Include API key to see same-tenant agents (not just public)
|
|
434
|
+
const response = await fetch(`${this.apiUrl}/api/discover/agents?${params}`, {
|
|
435
|
+
headers: {
|
|
436
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
437
|
+
},
|
|
250
438
|
});
|
|
439
|
+
return response.json();
|
|
440
|
+
}
|
|
251
441
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
442
|
+
/**
|
|
443
|
+
* Search agents with fuzzy matching on description
|
|
444
|
+
*/
|
|
445
|
+
async searchAgents(query: string, options?: Omit<DiscoverOptions, 'query'>): Promise<DiscoverResult> {
|
|
446
|
+
return this.discover({ ...options, query });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Find agents by category
|
|
451
|
+
*/
|
|
452
|
+
async findByCategory(category: string, options?: Omit<DiscoverOptions, 'category'>): Promise<DiscoverResult> {
|
|
453
|
+
return this.discover({ ...options, category });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Find agents by tag
|
|
458
|
+
*/
|
|
459
|
+
async findByTag(tag: string, options?: Omit<DiscoverOptions, 'tag'>): Promise<DiscoverResult> {
|
|
460
|
+
return this.discover({ ...options, tag });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Find agents by skill tag (searches within agent skills)
|
|
465
|
+
*/
|
|
466
|
+
async findBySkillTag(skillTag: string, options?: Omit<DiscoverOptions, 'skillTag'>): Promise<DiscoverResult> {
|
|
467
|
+
return this.discover({ ...options, skillTag });
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Find agents that support a specific input/output mode
|
|
472
|
+
*/
|
|
473
|
+
async findByContentMode(mode: string, options?: Omit<DiscoverOptions, 'contentMode'>): Promise<DiscoverResult> {
|
|
474
|
+
return this.discover({ ...options, contentMode: mode });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Get top-rated agents
|
|
479
|
+
*/
|
|
480
|
+
async getTopRated(limit = 10): Promise<DiscoverResult> {
|
|
481
|
+
return this.discover({ sort: 'rating', limit });
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Get most popular agents (by usage)
|
|
486
|
+
*/
|
|
487
|
+
async getPopular(limit = 10): Promise<DiscoverResult> {
|
|
488
|
+
return this.discover({ sort: 'popular', limit });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Get featured/curated agents
|
|
493
|
+
*/
|
|
494
|
+
async getFeatured(): Promise<{ featured: PublicAgent[] }> {
|
|
495
|
+
const response = await fetch(`${this.apiUrl}/api/discover/featured`);
|
|
496
|
+
return response.json();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Get available categories
|
|
501
|
+
*/
|
|
502
|
+
async getCategories(): Promise<{ categories: AgentCategory[] }> {
|
|
503
|
+
const response = await fetch(`${this.apiUrl}/api/discover/categories`);
|
|
504
|
+
return response.json();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Get detailed info about a public agent
|
|
509
|
+
*/
|
|
510
|
+
async getAgentInfo(agentId: string): Promise<AgentInfoResult> {
|
|
511
|
+
const response = await fetch(`${this.apiUrl}/api/discover/agents/${agentId}`);
|
|
512
|
+
if (!response.ok) {
|
|
513
|
+
throw new Error('Agent not found');
|
|
514
|
+
}
|
|
515
|
+
return response.json();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Rate an agent (requires authentication)
|
|
520
|
+
*/
|
|
521
|
+
async rateAgent(agentId: string, rating: number, review?: string): Promise<RatingResult> {
|
|
522
|
+
const response = await fetch(`${this.apiUrl}/api/discover/agents/${agentId}/rate`, {
|
|
523
|
+
method: 'POST',
|
|
524
|
+
headers: {
|
|
525
|
+
'Content-Type': 'application/json',
|
|
526
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
527
|
+
},
|
|
528
|
+
body: JSON.stringify({ rating, review }),
|
|
257
529
|
});
|
|
530
|
+
if (!response.ok) {
|
|
531
|
+
const error = await response.json();
|
|
532
|
+
throw new Error(error.error || 'Failed to rate agent');
|
|
533
|
+
}
|
|
534
|
+
return response.json();
|
|
535
|
+
}
|
|
258
536
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
537
|
+
/**
|
|
538
|
+
* Get best agent for a task using smart matching
|
|
539
|
+
* Searches by query and returns the top-rated match
|
|
540
|
+
*/
|
|
541
|
+
async findBestAgent(query: string, options?: {
|
|
542
|
+
category?: string;
|
|
543
|
+
minRating?: number;
|
|
544
|
+
pricing?: 'free' | 'paid' | 'any';
|
|
545
|
+
}): Promise<PublicAgent | null> {
|
|
546
|
+
const result = await this.discover({
|
|
547
|
+
query,
|
|
548
|
+
category: options?.category,
|
|
549
|
+
sort: 'rating',
|
|
550
|
+
limit: 10,
|
|
263
551
|
});
|
|
552
|
+
|
|
553
|
+
const agents = result.agents.filter(agent => {
|
|
554
|
+
if (options?.minRating && agent.avgRating < options.minRating) return false;
|
|
555
|
+
if (options?.pricing === 'free' && agent.pricing !== 'free') return false;
|
|
556
|
+
if (options?.pricing === 'paid' && agent.pricing === 'free') return false;
|
|
557
|
+
return true;
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
return agents[0] || null;
|
|
561
|
+
}
|
|
264
562
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
563
|
+
/**
|
|
564
|
+
* Find agents similar to a given agent
|
|
565
|
+
*/
|
|
566
|
+
async findSimilar(agentId: string, limit = 5): Promise<DiscoverResult> {
|
|
567
|
+
// Get the agent's info first
|
|
568
|
+
const info = await this.getAgentInfo(agentId);
|
|
569
|
+
const agent = info.agent;
|
|
570
|
+
|
|
571
|
+
// Search by category and tags
|
|
572
|
+
if (agent.category) {
|
|
573
|
+
const result = await this.discover({
|
|
574
|
+
category: agent.category,
|
|
575
|
+
sort: 'rating',
|
|
576
|
+
limit: limit + 1, // +1 to exclude self
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Filter out the original agent
|
|
580
|
+
result.agents = result.agents.filter(a => a.id !== agentId).slice(0, limit);
|
|
581
|
+
return result;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Fallback to top rated
|
|
585
|
+
return this.getTopRated(limit);
|
|
274
586
|
}
|
|
275
587
|
}
|
|
276
588
|
|
|
589
|
+
// ============================================================
|
|
590
|
+
// DISCOVERY TYPES
|
|
591
|
+
// ============================================================
|
|
592
|
+
|
|
593
|
+
export interface DiscoverOptions {
|
|
594
|
+
/** Search query (fuzzy matches name, description, tags) */
|
|
595
|
+
query?: string;
|
|
596
|
+
/** Filter by category */
|
|
597
|
+
category?: string;
|
|
598
|
+
/** Filter by tag */
|
|
599
|
+
tag?: string;
|
|
600
|
+
/** Filter by skill tag (searches within agent skills) */
|
|
601
|
+
skillTag?: string;
|
|
602
|
+
/** Filter by content mode (MIME type, e.g., 'text/markdown', 'image/png') */
|
|
603
|
+
contentMode?: string;
|
|
604
|
+
/** Sort order */
|
|
605
|
+
sort?: 'rating' | 'popular' | 'recent';
|
|
606
|
+
/** Max results (default 20, max 100) */
|
|
607
|
+
limit?: number;
|
|
608
|
+
/** Pagination offset */
|
|
609
|
+
offset?: number;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export interface DiscoverResult {
|
|
613
|
+
agents: PublicAgent[];
|
|
614
|
+
count: number;
|
|
615
|
+
offset: number;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export interface PublicAgent {
|
|
619
|
+
id: string;
|
|
620
|
+
name: string;
|
|
621
|
+
description: string | null;
|
|
622
|
+
category: string | null;
|
|
623
|
+
tags: string[];
|
|
624
|
+
pricing: 'free' | 'paid' | 'contact';
|
|
625
|
+
avgRating: number;
|
|
626
|
+
ratingCount: number;
|
|
627
|
+
tenantName: string;
|
|
628
|
+
websiteUrl: string | null;
|
|
629
|
+
docsUrl: string | null;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export interface AgentCategory {
|
|
633
|
+
name: string;
|
|
634
|
+
count: number;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export interface AgentInfoResult {
|
|
638
|
+
agent: PublicAgent & {
|
|
639
|
+
agentCard: {
|
|
640
|
+
name: string;
|
|
641
|
+
description?: string;
|
|
642
|
+
skills?: AgentSkill[];
|
|
643
|
+
} | null;
|
|
644
|
+
stats: {
|
|
645
|
+
avgRating: number;
|
|
646
|
+
ratingCount: number;
|
|
647
|
+
totalMessages: number;
|
|
648
|
+
successRate: number;
|
|
649
|
+
avgResponseTime: number;
|
|
650
|
+
};
|
|
651
|
+
};
|
|
652
|
+
reviews: AgentReview[];
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** Full A2A skill schema */
|
|
656
|
+
export interface AgentSkill {
|
|
657
|
+
id: string;
|
|
658
|
+
name: string;
|
|
659
|
+
description?: string;
|
|
660
|
+
tags?: string[];
|
|
661
|
+
examples?: string[];
|
|
662
|
+
inputModes?: string[];
|
|
663
|
+
outputModes?: string[];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export interface AgentReview {
|
|
667
|
+
rating: number;
|
|
668
|
+
review: string;
|
|
669
|
+
created_at: number;
|
|
670
|
+
reviewer_name: string;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export interface RatingResult {
|
|
674
|
+
success: boolean;
|
|
675
|
+
avgRating: number;
|
|
676
|
+
ratingCount: number;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Default export
|
|
277
680
|
export default GopherHole;
|