@gopherhole/sdk 0.1.0 → 0.1.2

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/index.js CHANGED
@@ -1,207 +1,459 @@
1
- import WebSocket from 'ws';
2
- import { EventEmitter } from 'events';
3
- export class GopherHole extends EventEmitter {
4
- apiKey;
5
- hubUrl;
6
- ws = null;
7
- reconnect;
8
- reconnectDelay;
9
- authenticated = false;
10
- pendingMessages = new Map();
11
- messageCounter = 0;
12
- agentId = null;
13
- constructor(apiKey, options = {}) {
14
- super();
15
- this.apiKey = apiKey;
16
- this.hubUrl = options.hubUrl || 'wss://gopherhole.ai/ws';
17
- this.reconnect = options.reconnect ?? true;
18
- this.reconnectDelay = options.reconnectDelay ?? 5000;
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ GopherHole: () => GopherHole,
24
+ JsonRpcErrorCodes: () => JsonRpcErrorCodes,
25
+ default: () => index_default
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+ var import_eventemitter3 = require("eventemitter3");
29
+
30
+ // src/types.ts
31
+ var JsonRpcErrorCodes = {
32
+ ParseError: -32700,
33
+ InvalidRequest: -32600,
34
+ MethodNotFound: -32601,
35
+ InvalidParams: -32602,
36
+ InternalError: -32603,
37
+ // A2A-specific errors
38
+ TaskNotFound: -32001,
39
+ TaskNotCancelable: -32002,
40
+ PushNotificationNotSupported: -32003,
41
+ UnsupportedOperation: -32004,
42
+ ContentTypeNotSupported: -32005,
43
+ InvalidAgentCard: -32006
44
+ };
45
+
46
+ // src/index.ts
47
+ var DEFAULT_HUB_URL = "wss://gopherhole.helixdata.workers.dev/ws";
48
+ var GopherHole = class extends import_eventemitter3.EventEmitter {
49
+ constructor(apiKeyOrOptions) {
50
+ super();
51
+ this.ws = null;
52
+ this.reconnectAttempts = 0;
53
+ this.reconnectTimer = null;
54
+ this.pingInterval = null;
55
+ this.agentId = null;
56
+ this.agentCard = null;
57
+ const options = typeof apiKeyOrOptions === "string" ? { apiKey: apiKeyOrOptions } : apiKeyOrOptions;
58
+ this.apiKey = options.apiKey;
59
+ this.hubUrl = options.hubUrl || DEFAULT_HUB_URL;
60
+ this.apiUrl = this.hubUrl.replace("/ws", "").replace("wss://", "https://").replace("ws://", "http://");
61
+ this.agentCard = options.agentCard || null;
62
+ this.autoReconnect = options.autoReconnect ?? true;
63
+ this.reconnectDelay = options.reconnectDelay ?? 1e3;
64
+ this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10;
65
+ }
66
+ /**
67
+ * Update agent card (sends to hub if connected)
68
+ */
69
+ async updateCard(card) {
70
+ this.agentCard = card;
71
+ if (this.ws?.readyState === 1) {
72
+ this.ws.send(JSON.stringify({ type: "update_card", agentCard: card }));
19
73
  }
20
- async connect() {
21
- return new Promise((resolve, reject) => {
22
- try {
23
- this.ws = new WebSocket(this.hubUrl);
24
- this.ws.on('open', () => {
25
- console.log('[GopherHole] Connected, authenticating...');
26
- this.ws.send(JSON.stringify({ type: 'auth', token: this.apiKey }));
27
- });
28
- this.ws.on('message', (data) => {
29
- try {
30
- const msg = JSON.parse(data.toString());
31
- this.handleMessage(msg, resolve, reject);
32
- }
33
- catch (err) {
34
- console.error('[GopherHole] Failed to parse message:', err);
35
- }
36
- });
37
- this.ws.on('close', () => {
38
- console.log('[GopherHole] Disconnected');
39
- this.authenticated = false;
40
- this.emit('disconnect');
41
- if (this.reconnect) {
42
- console.log(`[GopherHole] Reconnecting in ${this.reconnectDelay}ms...`);
43
- setTimeout(() => this.connect(), this.reconnectDelay);
44
- }
45
- });
46
- this.ws.on('error', (err) => {
47
- console.error('[GopherHole] WebSocket error:', err);
48
- this.emit('error', err);
49
- if (!this.authenticated) {
50
- reject(err);
51
- }
52
- });
53
- }
54
- catch (err) {
55
- reject(err);
56
- }
57
- });
58
- }
59
- handleMessage(msg, resolve, reject) {
60
- switch (msg.type) {
61
- case 'auth_ok':
62
- console.log(`[GopherHole] Authenticated as ${msg.agentId}`);
63
- this.authenticated = true;
64
- this.agentId = msg.agentId || null;
65
- this.emit('connected', { agentId: this.agentId });
66
- resolve?.();
67
- break;
68
- case 'auth_error':
69
- console.error('[GopherHole] Auth failed:', msg.error);
70
- reject?.(new Error(msg.error || 'Authentication failed'));
71
- break;
72
- case 'message':
73
- const incomingMsg = {
74
- from: msg.from,
75
- to: msg.to,
76
- payload: msg.payload,
77
- timestamp: msg.timestamp || Date.now(),
78
- };
79
- console.log(`[GopherHole] Message from ${msg.from}`);
80
- this.emit('message', incomingMsg);
81
- break;
82
- case 'ack':
83
- const pending = this.pendingMessages.get(msg.id);
84
- if (pending) {
85
- pending.resolve({ id: msg.id, timestamp: msg.timestamp });
86
- this.pendingMessages.delete(msg.id);
87
- }
88
- break;
89
- case 'error':
90
- console.error('[GopherHole] Error:', msg.error);
91
- const pendingErr = msg.id ? this.pendingMessages.get(msg.id) : null;
92
- if (pendingErr) {
93
- pendingErr.reject(new Error(msg.error));
94
- this.pendingMessages.delete(msg.id);
95
- }
96
- this.emit('error', new Error(msg.error));
97
- break;
98
- default:
99
- console.log('[GopherHole] Unknown message type:', msg.type);
74
+ }
75
+ /**
76
+ * Connect to the GopherHole hub via WebSocket
77
+ */
78
+ async connect() {
79
+ return new Promise((resolve, reject) => {
80
+ const WS = typeof WebSocket !== "undefined" ? WebSocket : require("ws");
81
+ const ws = new WS(this.hubUrl, {
82
+ headers: {
83
+ "Authorization": `Bearer ${this.apiKey}`
100
84
  }
101
- }
102
- async send(to, payload) {
103
- if (!this.authenticated || !this.ws) {
104
- throw new Error('Not connected');
85
+ });
86
+ this.ws = ws;
87
+ ws.onopen = () => {
88
+ this.reconnectAttempts = 0;
89
+ this.startPing();
90
+ this.emit("connect");
91
+ resolve();
92
+ };
93
+ ws.onclose = (event) => {
94
+ this.stopPing();
95
+ const reason = event.reason || "Connection closed";
96
+ this.emit("disconnect", reason);
97
+ if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
98
+ this.scheduleReconnect();
105
99
  }
106
- const id = `msg-${++this.messageCounter}-${Date.now()}`;
107
- return new Promise((resolve, reject) => {
108
- this.pendingMessages.set(id, { resolve: resolve, reject });
109
- this.ws.send(JSON.stringify({
110
- type: 'message',
111
- id,
112
- to,
113
- payload,
114
- }));
115
- // Timeout after 30s
116
- setTimeout(() => {
117
- if (this.pendingMessages.has(id)) {
118
- this.pendingMessages.delete(id);
119
- reject(new Error('Message send timeout'));
120
- }
121
- }, 30000);
122
- });
100
+ };
101
+ ws.onerror = () => {
102
+ const error = new Error("WebSocket error");
103
+ this.emit("error", error);
104
+ reject(error);
105
+ };
106
+ ws.onmessage = (event) => {
107
+ try {
108
+ const data = JSON.parse(event.data.toString());
109
+ this.handleMessage(data);
110
+ } catch {
111
+ this.emit("error", new Error("Failed to parse message"));
112
+ }
113
+ };
114
+ });
115
+ }
116
+ /**
117
+ * Disconnect from the hub
118
+ */
119
+ disconnect() {
120
+ this.autoReconnect = false;
121
+ this.stopPing();
122
+ if (this.reconnectTimer) {
123
+ clearTimeout(this.reconnectTimer);
124
+ this.reconnectTimer = null;
123
125
  }
124
- sendText(to, text, contextId) {
125
- return this.send(to, {
126
- parts: [{ kind: 'text', text }],
127
- contextId,
128
- });
126
+ if (this.ws) {
127
+ this.ws.close();
128
+ this.ws = null;
129
129
  }
130
- disconnect() {
131
- this.reconnect = false;
132
- if (this.ws) {
133
- this.ws.close();
134
- this.ws = null;
130
+ }
131
+ /**
132
+ * Send a message to another agent
133
+ */
134
+ async send(toAgentId, payload, options) {
135
+ const response = await this.rpc("message/send", {
136
+ message: payload,
137
+ configuration: {
138
+ agentId: toAgentId,
139
+ ...options
140
+ }
141
+ });
142
+ return response;
143
+ }
144
+ /**
145
+ * Send a text message to another agent
146
+ */
147
+ async sendText(toAgentId, text, options) {
148
+ return this.send(toAgentId, {
149
+ role: "agent",
150
+ parts: [{ kind: "text", text }]
151
+ }, options);
152
+ }
153
+ /**
154
+ * Get a task by ID
155
+ */
156
+ async getTask(taskId, historyLength) {
157
+ const response = await this.rpc("tasks/get", {
158
+ id: taskId,
159
+ historyLength
160
+ });
161
+ return response;
162
+ }
163
+ /**
164
+ * List tasks
165
+ */
166
+ async listTasks(options) {
167
+ const response = await this.rpc("tasks/list", options || {});
168
+ return response;
169
+ }
170
+ /**
171
+ * Cancel a task
172
+ */
173
+ async cancelTask(taskId) {
174
+ const response = await this.rpc("tasks/cancel", { id: taskId });
175
+ return response;
176
+ }
177
+ /**
178
+ * Reply to a message/task (sends back to the original caller)
179
+ */
180
+ async reply(taskId, payload, toAgentId) {
181
+ if (!toAgentId) {
182
+ const task = await this.getTask(taskId);
183
+ const response2 = await this.rpc("message/send", {
184
+ message: payload,
185
+ configuration: {
186
+ contextId: task.contextId
187
+ // Server needs to handle replies via context routing
135
188
  }
189
+ });
190
+ return response2;
136
191
  }
137
- isConnected() {
138
- return this.authenticated && this.ws?.readyState === WebSocket.OPEN;
192
+ const response = await this.rpc("message/send", {
193
+ message: payload,
194
+ configuration: {
195
+ agentId: toAgentId
196
+ }
197
+ });
198
+ return response;
199
+ }
200
+ /**
201
+ * Reply with text
202
+ */
203
+ async replyText(taskId, text) {
204
+ return this.reply(taskId, {
205
+ role: "agent",
206
+ parts: [{ kind: "text", text }]
207
+ });
208
+ }
209
+ /**
210
+ * Make a JSON-RPC call to the A2A endpoint
211
+ */
212
+ async rpc(method, params) {
213
+ const response = await fetch(`${this.apiUrl}/a2a`, {
214
+ method: "POST",
215
+ headers: {
216
+ "Content-Type": "application/json",
217
+ "Authorization": `Bearer ${this.apiKey}`
218
+ },
219
+ body: JSON.stringify({
220
+ jsonrpc: "2.0",
221
+ method,
222
+ params,
223
+ id: Date.now()
224
+ })
225
+ });
226
+ const data = await response.json();
227
+ if (data.error) {
228
+ throw new Error(data.error.message || "RPC error");
139
229
  }
140
- /**
141
- * Create a GopherHole instance from environment variables
142
- * Reads GOPHERHOLE_API_KEY and optionally GOPHERHOLE_HUB_URL
143
- */
144
- static fromEnv(options = {}) {
145
- const apiKey = process.env.GOPHERHOLE_API_KEY;
146
- if (!apiKey) {
147
- throw new Error('GOPHERHOLE_API_KEY environment variable is required');
148
- }
149
- return new GopherHole(apiKey, {
150
- ...options,
151
- hubUrl: process.env.GOPHERHOLE_HUB_URL || 'wss://gopherhole.ai/ws',
152
- });
230
+ return data.result;
231
+ }
232
+ /**
233
+ * Handle incoming WebSocket messages
234
+ */
235
+ handleMessage(data) {
236
+ if (data.type === "message") {
237
+ this.emit("message", {
238
+ from: data.from,
239
+ taskId: data.taskId,
240
+ payload: data.payload,
241
+ timestamp: data.timestamp || Date.now()
242
+ });
243
+ } else if (data.type === "task_update") {
244
+ this.emit("taskUpdate", data.task);
245
+ } else if (data.type === "pong") {
246
+ } else if (data.type === "welcome") {
247
+ this.agentId = data.agentId;
248
+ if (this.agentCard && this.ws?.readyState === 1) {
249
+ this.ws.send(JSON.stringify({ type: "update_card", agentCard: this.agentCard }));
250
+ }
251
+ } else if (data.type === "card_updated") {
252
+ } else if (data.type === "warning") {
253
+ console.warn("GopherHole warning:", data.message);
153
254
  }
154
- /**
155
- * Create a simple agent with a message handler
156
- */
157
- static agent(config) {
158
- const apiKey = config.apiKey || process.env.GOPHERHOLE_API_KEY;
159
- if (!apiKey) {
160
- throw new Error('API key required: pass apiKey or set GOPHERHOLE_API_KEY');
161
- }
162
- const hub = new GopherHole(apiKey, {
163
- hubUrl: config.hubUrl || process.env.GOPHERHOLE_HUB_URL,
164
- });
165
- hub.on('message', async (msg) => {
166
- try {
167
- const response = await config.onMessage(msg, hub);
168
- // If handler returns a response, send it back
169
- if (response) {
170
- if (typeof response === 'string') {
171
- await hub.sendText(msg.from, response);
172
- }
173
- else {
174
- await hub.send(msg.from, response);
175
- }
176
- }
177
- }
178
- catch (err) {
179
- console.error('[GopherHole Agent] Message handler error:', err);
180
- if (config.onError) {
181
- config.onError(err, hub);
182
- }
183
- }
184
- });
185
- hub.on('connected', () => {
186
- console.log(`[GopherHole Agent] Connected as ${hub.agentId}`);
187
- if (config.onConnect) {
188
- config.onConnect(hub);
189
- }
190
- });
191
- hub.on('error', (err) => {
192
- if (config.onError) {
193
- config.onError(err, hub);
194
- }
195
- });
196
- return {
197
- hub,
198
- start: async () => {
199
- await hub.connect();
200
- },
201
- stop: () => {
202
- hub.disconnect();
203
- },
204
- };
255
+ }
256
+ /**
257
+ * Start ping interval
258
+ */
259
+ startPing() {
260
+ this.pingInterval = setInterval(() => {
261
+ if (this.ws?.readyState === 1) {
262
+ this.ws.send(JSON.stringify({ type: "ping" }));
263
+ }
264
+ }, 3e4);
265
+ }
266
+ /**
267
+ * Stop ping interval
268
+ */
269
+ stopPing() {
270
+ if (this.pingInterval) {
271
+ clearInterval(this.pingInterval);
272
+ this.pingInterval = null;
273
+ }
274
+ }
275
+ /**
276
+ * Schedule reconnection
277
+ */
278
+ scheduleReconnect() {
279
+ if (this.reconnectTimer) return;
280
+ this.reconnectAttempts++;
281
+ const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
282
+ this.reconnectTimer = setTimeout(async () => {
283
+ this.reconnectTimer = null;
284
+ try {
285
+ await this.connect();
286
+ } catch {
287
+ }
288
+ }, delay);
289
+ }
290
+ /**
291
+ * Get connection state
292
+ */
293
+ get connected() {
294
+ return this.ws?.readyState === 1;
295
+ }
296
+ /**
297
+ * Get the agent ID (available after connect)
298
+ */
299
+ get id() {
300
+ return this.agentId;
301
+ }
302
+ // ============================================================
303
+ // DISCOVERY METHODS
304
+ // ============================================================
305
+ /**
306
+ * Discover public agents with comprehensive search
307
+ */
308
+ async discover(options) {
309
+ const params = new URLSearchParams();
310
+ if (options?.query) params.set("q", options.query);
311
+ if (options?.category) params.set("category", options.category);
312
+ if (options?.tag) params.set("tag", options.tag);
313
+ if (options?.skillTag) params.set("skillTag", options.skillTag);
314
+ if (options?.contentMode) params.set("contentMode", options.contentMode);
315
+ if (options?.sort) params.set("sort", options.sort);
316
+ if (options?.limit) params.set("limit", String(options.limit));
317
+ if (options?.offset) params.set("offset", String(options.offset));
318
+ if (options?.scope) params.set("scope", options.scope);
319
+ const response = await fetch(`${this.apiUrl}/api/discover/agents?${params}`, {
320
+ headers: {
321
+ "Authorization": `Bearer ${this.apiKey}`
322
+ }
323
+ });
324
+ return response.json();
325
+ }
326
+ /**
327
+ * Discover all agents in your tenant (no limit)
328
+ */
329
+ async discoverTenantAgents() {
330
+ return this.discover({ scope: "tenant" });
331
+ }
332
+ /**
333
+ * Search agents with fuzzy matching on description
334
+ */
335
+ async searchAgents(query, options) {
336
+ return this.discover({ ...options, query });
337
+ }
338
+ /**
339
+ * Find agents by category
340
+ */
341
+ async findByCategory(category, options) {
342
+ return this.discover({ ...options, category });
343
+ }
344
+ /**
345
+ * Find agents by tag
346
+ */
347
+ async findByTag(tag, options) {
348
+ return this.discover({ ...options, tag });
349
+ }
350
+ /**
351
+ * Find agents by skill tag (searches within agent skills)
352
+ */
353
+ async findBySkillTag(skillTag, options) {
354
+ return this.discover({ ...options, skillTag });
355
+ }
356
+ /**
357
+ * Find agents that support a specific input/output mode
358
+ */
359
+ async findByContentMode(mode, options) {
360
+ return this.discover({ ...options, contentMode: mode });
361
+ }
362
+ /**
363
+ * Get top-rated agents
364
+ */
365
+ async getTopRated(limit = 10) {
366
+ return this.discover({ sort: "rating", limit });
367
+ }
368
+ /**
369
+ * Get most popular agents (by usage)
370
+ */
371
+ async getPopular(limit = 10) {
372
+ return this.discover({ sort: "popular", limit });
373
+ }
374
+ /**
375
+ * Get featured/curated agents
376
+ */
377
+ async getFeatured() {
378
+ const response = await fetch(`${this.apiUrl}/api/discover/featured`);
379
+ return response.json();
380
+ }
381
+ /**
382
+ * Get available categories
383
+ */
384
+ async getCategories() {
385
+ const response = await fetch(`${this.apiUrl}/api/discover/categories`);
386
+ return response.json();
387
+ }
388
+ /**
389
+ * Get detailed info about a public agent
390
+ */
391
+ async getAgentInfo(agentId) {
392
+ const response = await fetch(`${this.apiUrl}/api/discover/agents/${agentId}`);
393
+ if (!response.ok) {
394
+ throw new Error("Agent not found");
395
+ }
396
+ return response.json();
397
+ }
398
+ /**
399
+ * Rate an agent (requires authentication)
400
+ */
401
+ async rateAgent(agentId, rating, review) {
402
+ const response = await fetch(`${this.apiUrl}/api/discover/agents/${agentId}/rate`, {
403
+ method: "POST",
404
+ headers: {
405
+ "Content-Type": "application/json",
406
+ "Authorization": `Bearer ${this.apiKey}`
407
+ },
408
+ body: JSON.stringify({ rating, review })
409
+ });
410
+ if (!response.ok) {
411
+ const error = await response.json();
412
+ throw new Error(error.error || "Failed to rate agent");
413
+ }
414
+ return response.json();
415
+ }
416
+ /**
417
+ * Get best agent for a task using smart matching
418
+ * Searches by query and returns the top-rated match
419
+ */
420
+ async findBestAgent(query, options) {
421
+ const result = await this.discover({
422
+ query,
423
+ category: options?.category,
424
+ sort: "rating",
425
+ limit: 10
426
+ });
427
+ const agents = result.agents.filter((agent) => {
428
+ if (options?.minRating && agent.avgRating < options.minRating) return false;
429
+ if (options?.pricing === "free" && agent.pricing !== "free") return false;
430
+ if (options?.pricing === "paid" && agent.pricing === "free") return false;
431
+ return true;
432
+ });
433
+ return agents[0] || null;
434
+ }
435
+ /**
436
+ * Find agents similar to a given agent
437
+ */
438
+ async findSimilar(agentId, limit = 5) {
439
+ const info = await this.getAgentInfo(agentId);
440
+ const agent = info.agent;
441
+ if (agent.category) {
442
+ const result = await this.discover({
443
+ category: agent.category,
444
+ sort: "rating",
445
+ limit: limit + 1
446
+ // +1 to exclude self
447
+ });
448
+ result.agents = result.agents.filter((a) => a.id !== agentId).slice(0, limit);
449
+ return result;
205
450
  }
206
- }
207
- export default GopherHole;
451
+ return this.getTopRated(limit);
452
+ }
453
+ };
454
+ var index_default = GopherHole;
455
+ // Annotate the CommonJS export names for ESM import in node:
456
+ 0 && (module.exports = {
457
+ GopherHole,
458
+ JsonRpcErrorCodes
459
+ });