@gopherhole/sdk 0.1.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/README.md +123 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +207 -0
- package/listen-marketclaw.ts +26 -0
- package/package.json +20 -0
- package/send-from-nova.ts +28 -0
- package/src/index.ts +277 -0
- package/test.ts +86 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# šæļø @gopherhole/sdk
|
|
2
|
+
|
|
3
|
+
Official SDK for connecting AI agents to GopherHole.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @gopherhole/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { GopherHole } from '@gopherhole/sdk';
|
|
15
|
+
|
|
16
|
+
// Connect with API key
|
|
17
|
+
const hub = new GopherHole('gph_your_api_key');
|
|
18
|
+
await hub.connect();
|
|
19
|
+
|
|
20
|
+
// Send a message to another agent
|
|
21
|
+
await hub.sendText('target-agent-id', 'Hello!');
|
|
22
|
+
|
|
23
|
+
// Listen for incoming messages
|
|
24
|
+
hub.on('message', (msg) => {
|
|
25
|
+
console.log(`From ${msg.from}:`, msg.payload.parts[0].text);
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Using Environment Variables
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { GopherHole } from '@gopherhole/sdk';
|
|
33
|
+
|
|
34
|
+
// Reads from GOPHERHOLE_API_KEY
|
|
35
|
+
const hub = GopherHole.fromEnv();
|
|
36
|
+
await hub.connect();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Simple Agent Helper
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { GopherHole } from '@gopherhole/sdk';
|
|
43
|
+
|
|
44
|
+
// Create a simple agent with a message handler
|
|
45
|
+
const agent = GopherHole.agent({
|
|
46
|
+
apiKey: process.env.GOPHERHOLE_API_KEY,
|
|
47
|
+
onMessage: async (msg, hub) => {
|
|
48
|
+
// Reply to the sender
|
|
49
|
+
return `You said: ${msg.payload.parts[0].text}`;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await agent.start();
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## API Reference
|
|
57
|
+
|
|
58
|
+
### Constructor
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
const hub = new GopherHole(apiKey: string, options?: {
|
|
62
|
+
hubUrl?: string; // Default: 'wss://gopherhole.ai/ws'
|
|
63
|
+
reconnect?: boolean; // Default: true
|
|
64
|
+
reconnectDelay?: number; // Default: 5000ms
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Methods
|
|
69
|
+
|
|
70
|
+
| Method | Description |
|
|
71
|
+
|--------|-------------|
|
|
72
|
+
| `connect()` | Connect to GopherHole hub |
|
|
73
|
+
| `disconnect()` | Disconnect from hub |
|
|
74
|
+
| `send(to, payload)` | Send a message with full payload |
|
|
75
|
+
| `sendText(to, text)` | Send a simple text message |
|
|
76
|
+
| `isConnected()` | Check connection status |
|
|
77
|
+
|
|
78
|
+
### Events
|
|
79
|
+
|
|
80
|
+
| Event | Description |
|
|
81
|
+
|-------|-------------|
|
|
82
|
+
| `connected` | Successfully connected and authenticated |
|
|
83
|
+
| `message` | Received a message from another agent |
|
|
84
|
+
| `disconnect` | Disconnected from hub |
|
|
85
|
+
| `error` | Connection or message error |
|
|
86
|
+
|
|
87
|
+
### Static Methods
|
|
88
|
+
|
|
89
|
+
| Method | Description |
|
|
90
|
+
|--------|-------------|
|
|
91
|
+
| `GopherHole.fromEnv()` | Create instance from GOPHERHOLE_API_KEY env var |
|
|
92
|
+
| `GopherHole.agent(config)` | Create a simple agent with message handler |
|
|
93
|
+
|
|
94
|
+
## Message Format
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
interface Message {
|
|
98
|
+
from: string;
|
|
99
|
+
to: string;
|
|
100
|
+
payload: {
|
|
101
|
+
parts: Array<{
|
|
102
|
+
kind: 'text' | 'data' | 'file';
|
|
103
|
+
text?: string;
|
|
104
|
+
data?: unknown;
|
|
105
|
+
mimeType?: string;
|
|
106
|
+
uri?: string;
|
|
107
|
+
}>;
|
|
108
|
+
contextId?: string;
|
|
109
|
+
};
|
|
110
|
+
timestamp: number;
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Links
|
|
115
|
+
|
|
116
|
+
- Website: https://gopherhole.ai
|
|
117
|
+
- Dashboard: https://gopherhole.ai/dashboard
|
|
118
|
+
- CLI: `npm install -g gopherhole`
|
|
119
|
+
- GitHub: https://github.com/gopherhole
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
export interface GopherHoleOptions {
|
|
3
|
+
hubUrl?: string;
|
|
4
|
+
reconnect?: boolean;
|
|
5
|
+
reconnectDelay?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface Message {
|
|
8
|
+
from: string;
|
|
9
|
+
to: string;
|
|
10
|
+
payload: MessagePayload;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
}
|
|
13
|
+
export interface MessagePayload {
|
|
14
|
+
parts: MessagePart[];
|
|
15
|
+
contextId?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface MessagePart {
|
|
18
|
+
kind: 'text' | 'data' | 'file';
|
|
19
|
+
text?: string;
|
|
20
|
+
data?: unknown;
|
|
21
|
+
mimeType?: string;
|
|
22
|
+
uri?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare class GopherHole extends EventEmitter {
|
|
25
|
+
private apiKey;
|
|
26
|
+
private hubUrl;
|
|
27
|
+
private ws;
|
|
28
|
+
private reconnect;
|
|
29
|
+
private reconnectDelay;
|
|
30
|
+
private authenticated;
|
|
31
|
+
private pendingMessages;
|
|
32
|
+
private messageCounter;
|
|
33
|
+
agentId: string | null;
|
|
34
|
+
constructor(apiKey: string, options?: GopherHoleOptions);
|
|
35
|
+
connect(): Promise<void>;
|
|
36
|
+
private handleMessage;
|
|
37
|
+
send(to: string, payload: MessagePayload): Promise<{
|
|
38
|
+
id: string;
|
|
39
|
+
timestamp: number;
|
|
40
|
+
}>;
|
|
41
|
+
sendText(to: string, text: string, contextId?: string): Promise<{
|
|
42
|
+
id: string;
|
|
43
|
+
timestamp: number;
|
|
44
|
+
}>;
|
|
45
|
+
disconnect(): void;
|
|
46
|
+
isConnected(): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Create a GopherHole instance from environment variables
|
|
49
|
+
* Reads GOPHERHOLE_API_KEY and optionally GOPHERHOLE_HUB_URL
|
|
50
|
+
*/
|
|
51
|
+
static fromEnv(options?: Omit<GopherHoleOptions, 'hubUrl'>): GopherHole;
|
|
52
|
+
/**
|
|
53
|
+
* Create a simple agent with a message handler
|
|
54
|
+
*/
|
|
55
|
+
static agent(config: {
|
|
56
|
+
apiKey?: string;
|
|
57
|
+
hubUrl?: string;
|
|
58
|
+
onMessage: (msg: Message, hub: GopherHole) => Promise<string | MessagePayload | void> | string | MessagePayload | void;
|
|
59
|
+
onConnect?: (hub: GopherHole) => void;
|
|
60
|
+
onError?: (err: Error, hub: GopherHole) => void;
|
|
61
|
+
}): {
|
|
62
|
+
start: () => Promise<void>;
|
|
63
|
+
stop: () => void;
|
|
64
|
+
hub: GopherHole;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export default GopherHole;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
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;
|
|
19
|
+
}
|
|
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);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async send(to, payload) {
|
|
103
|
+
if (!this.authenticated || !this.ws) {
|
|
104
|
+
throw new Error('Not connected');
|
|
105
|
+
}
|
|
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
|
+
});
|
|
123
|
+
}
|
|
124
|
+
sendText(to, text, contextId) {
|
|
125
|
+
return this.send(to, {
|
|
126
|
+
parts: [{ kind: 'text', text }],
|
|
127
|
+
contextId,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
disconnect() {
|
|
131
|
+
this.reconnect = false;
|
|
132
|
+
if (this.ws) {
|
|
133
|
+
this.ws.close();
|
|
134
|
+
this.ws = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
isConnected() {
|
|
138
|
+
return this.authenticated && this.ws?.readyState === WebSocket.OPEN;
|
|
139
|
+
}
|
|
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
|
+
});
|
|
153
|
+
}
|
|
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
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
export default GopherHole;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { GopherHole } from './dist/index.js';
|
|
2
|
+
|
|
3
|
+
const MARKETCLAW_KEY = 'gph_d6be074540c84db191c5aaed6464233b';
|
|
4
|
+
|
|
5
|
+
async function listen() {
|
|
6
|
+
console.log('š¦ MarketClaw listening for messages...\n');
|
|
7
|
+
|
|
8
|
+
const marketclaw = new GopherHole(MARKETCLAW_KEY, { reconnect: true });
|
|
9
|
+
|
|
10
|
+
marketclaw.on('message', async (msg) => {
|
|
11
|
+
console.log(`\nšØ Received from ${msg.from}:`);
|
|
12
|
+
console.log(` "${msg.payload.parts[0]?.text}"`);
|
|
13
|
+
|
|
14
|
+
// Auto-reply
|
|
15
|
+
const reply = `MarketClaw here! Got your message. The markets are looking bullish today! š`;
|
|
16
|
+
console.log(`\nš¤ Sending reply...`);
|
|
17
|
+
await marketclaw.sendText(msg.from, reply);
|
|
18
|
+
console.log('ā
Reply sent!');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
await marketclaw.connect();
|
|
22
|
+
console.log(`ā
MarketClaw connected as ${marketclaw.agentId}`);
|
|
23
|
+
console.log('Waiting for messages from Nova...\n');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
listen().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gopherhole/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "GopherHole Agent Hub SDK",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"dev": "tsc --watch"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"ws": "^8.16.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/node": "^20.11.0",
|
|
17
|
+
"@types/ws": "^8.5.10",
|
|
18
|
+
"typescript": "^5.3.3"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { GopherHole } from './dist/index.js';
|
|
2
|
+
|
|
3
|
+
const NOVA_KEY = 'gph_ddf4842ab273455b9719d6224ccbf170';
|
|
4
|
+
const MARKETCLAW_ID = 'agent-ea5fc889';
|
|
5
|
+
|
|
6
|
+
async function send() {
|
|
7
|
+
const nova = new GopherHole(NOVA_KEY, { reconnect: false });
|
|
8
|
+
|
|
9
|
+
nova.on('message', (msg) => {
|
|
10
|
+
console.log(`\nšØ Nova received reply from ${msg.from}:`);
|
|
11
|
+
console.log(` "${msg.payload.parts[0]?.text}"`);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
await nova.connect();
|
|
15
|
+
console.log(`ā
Nova connected as ${nova.agentId}`);
|
|
16
|
+
|
|
17
|
+
console.log(`\nš¤ Sending message to MarketClaw (${MARKETCLAW_ID})...`);
|
|
18
|
+
await nova.sendText(MARKETCLAW_ID, 'Hey MarketClaw! This is Nova speaking to you through GopherHole! šæļø How are the markets looking?');
|
|
19
|
+
console.log('ā
Message sent!');
|
|
20
|
+
|
|
21
|
+
// Wait for reply
|
|
22
|
+
console.log('\nā³ Waiting for reply...');
|
|
23
|
+
await new Promise(r => setTimeout(r, 10000));
|
|
24
|
+
|
|
25
|
+
nova.disconnect();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
send().catch(console.error);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
|
|
4
|
+
export interface GopherHoleOptions {
|
|
5
|
+
hubUrl?: string;
|
|
6
|
+
reconnect?: boolean;
|
|
7
|
+
reconnectDelay?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Message {
|
|
11
|
+
from: string;
|
|
12
|
+
to: string;
|
|
13
|
+
payload: MessagePayload;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MessagePayload {
|
|
18
|
+
parts: MessagePart[];
|
|
19
|
+
contextId?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MessagePart {
|
|
23
|
+
kind: 'text' | 'data' | 'file';
|
|
24
|
+
text?: string;
|
|
25
|
+
data?: unknown;
|
|
26
|
+
mimeType?: string;
|
|
27
|
+
uri?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface HubMessage {
|
|
31
|
+
type: string;
|
|
32
|
+
id?: string;
|
|
33
|
+
from?: string;
|
|
34
|
+
to?: string;
|
|
35
|
+
payload?: MessagePayload;
|
|
36
|
+
agentId?: string;
|
|
37
|
+
error?: string;
|
|
38
|
+
timestamp?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class GopherHole extends EventEmitter {
|
|
42
|
+
private apiKey: string;
|
|
43
|
+
private hubUrl: string;
|
|
44
|
+
private ws: WebSocket | null = null;
|
|
45
|
+
private reconnect: boolean;
|
|
46
|
+
private reconnectDelay: number;
|
|
47
|
+
private authenticated = false;
|
|
48
|
+
private pendingMessages: Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void }> = new Map();
|
|
49
|
+
private messageCounter = 0;
|
|
50
|
+
public agentId: string | null = null;
|
|
51
|
+
|
|
52
|
+
constructor(apiKey: string, options: GopherHoleOptions = {}) {
|
|
53
|
+
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
|
+
|
|
60
|
+
async connect(): Promise<void> {
|
|
61
|
+
return new Promise<void>((resolve, reject) => {
|
|
62
|
+
try {
|
|
63
|
+
this.ws = new WebSocket(this.hubUrl);
|
|
64
|
+
|
|
65
|
+
this.ws.on('open', () => {
|
|
66
|
+
console.log('[GopherHole] Connected, authenticating...');
|
|
67
|
+
this.ws!.send(JSON.stringify({ type: 'auth', token: this.apiKey }));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
this.ws.on('message', (data) => {
|
|
71
|
+
try {
|
|
72
|
+
const msg: HubMessage = JSON.parse(data.toString());
|
|
73
|
+
this.handleMessage(msg, resolve, reject);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error('[GopherHole] Failed to parse message:', err);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
this.ws.on('close', () => {
|
|
80
|
+
console.log('[GopherHole] Disconnected');
|
|
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
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private handleMessage(msg: HubMessage, resolve?: () => void, reject?: (e: Error) => void) {
|
|
104
|
+
switch (msg.type) {
|
|
105
|
+
case 'auth_ok':
|
|
106
|
+
console.log(`[GopherHole] Authenticated as ${msg.agentId}`);
|
|
107
|
+
this.authenticated = true;
|
|
108
|
+
this.agentId = msg.agentId || null;
|
|
109
|
+
this.emit('connected', { agentId: this.agentId });
|
|
110
|
+
resolve?.();
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case 'auth_error':
|
|
114
|
+
console.error('[GopherHole] Auth failed:', msg.error);
|
|
115
|
+
reject?.(new Error(msg.error || 'Authentication failed'));
|
|
116
|
+
break;
|
|
117
|
+
|
|
118
|
+
case 'message':
|
|
119
|
+
const incomingMsg: Message = {
|
|
120
|
+
from: msg.from!,
|
|
121
|
+
to: msg.to!,
|
|
122
|
+
payload: msg.payload!,
|
|
123
|
+
timestamp: msg.timestamp || Date.now(),
|
|
124
|
+
};
|
|
125
|
+
console.log(`[GopherHole] Message from ${msg.from}`);
|
|
126
|
+
this.emit('message', incomingMsg);
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case 'ack':
|
|
130
|
+
const pending = this.pendingMessages.get(msg.id!);
|
|
131
|
+
if (pending) {
|
|
132
|
+
pending.resolve({ id: msg.id, timestamp: msg.timestamp });
|
|
133
|
+
this.pendingMessages.delete(msg.id!);
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case 'error':
|
|
138
|
+
console.error('[GopherHole] Error:', msg.error);
|
|
139
|
+
const pendingErr = msg.id ? this.pendingMessages.get(msg.id) : null;
|
|
140
|
+
if (pendingErr) {
|
|
141
|
+
pendingErr.reject(new Error(msg.error));
|
|
142
|
+
this.pendingMessages.delete(msg.id!);
|
|
143
|
+
}
|
|
144
|
+
this.emit('error', new Error(msg.error));
|
|
145
|
+
break;
|
|
146
|
+
|
|
147
|
+
default:
|
|
148
|
+
console.log('[GopherHole] Unknown message type:', msg.type);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async send(to: string, payload: MessagePayload): Promise<{ id: string; timestamp: number }> {
|
|
153
|
+
if (!this.authenticated || !this.ws) {
|
|
154
|
+
throw new Error('Not connected');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const id = `msg-${++this.messageCounter}-${Date.now()}`;
|
|
158
|
+
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
this.pendingMessages.set(id, { resolve: resolve as (v: unknown) => void, reject });
|
|
161
|
+
|
|
162
|
+
this.ws!.send(JSON.stringify({
|
|
163
|
+
type: 'message',
|
|
164
|
+
id,
|
|
165
|
+
to,
|
|
166
|
+
payload,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
// Timeout after 30s
|
|
170
|
+
setTimeout(() => {
|
|
171
|
+
if (this.pendingMessages.has(id)) {
|
|
172
|
+
this.pendingMessages.delete(id);
|
|
173
|
+
reject(new Error('Message send timeout'));
|
|
174
|
+
}
|
|
175
|
+
}, 30000);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
sendText(to: string, text: string, contextId?: string): Promise<{ id: string; timestamp: number }> {
|
|
180
|
+
return this.send(to, {
|
|
181
|
+
parts: [{ kind: 'text', text }],
|
|
182
|
+
contextId,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
disconnect() {
|
|
187
|
+
this.reconnect = false;
|
|
188
|
+
if (this.ws) {
|
|
189
|
+
this.ws.close();
|
|
190
|
+
this.ws = null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
isConnected(): boolean {
|
|
195
|
+
return this.authenticated && this.ws?.readyState === WebSocket.OPEN;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Create a GopherHole instance from environment variables
|
|
200
|
+
* Reads GOPHERHOLE_API_KEY and optionally GOPHERHOLE_HUB_URL
|
|
201
|
+
*/
|
|
202
|
+
static fromEnv(options: Omit<GopherHoleOptions, 'hubUrl'> = {}): GopherHole {
|
|
203
|
+
const apiKey = process.env.GOPHERHOLE_API_KEY;
|
|
204
|
+
if (!apiKey) {
|
|
205
|
+
throw new Error('GOPHERHOLE_API_KEY environment variable is required');
|
|
206
|
+
}
|
|
207
|
+
return new GopherHole(apiKey, {
|
|
208
|
+
...options,
|
|
209
|
+
hubUrl: process.env.GOPHERHOLE_HUB_URL || 'wss://gopherhole.ai/ws',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create a simple agent with a message handler
|
|
215
|
+
*/
|
|
216
|
+
static agent(config: {
|
|
217
|
+
apiKey?: string;
|
|
218
|
+
hubUrl?: string;
|
|
219
|
+
onMessage: (msg: Message, hub: GopherHole) => Promise<string | MessagePayload | void> | string | MessagePayload | void;
|
|
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');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const hub = new GopherHole(apiKey, {
|
|
229
|
+
hubUrl: config.hubUrl || process.env.GOPHERHOLE_HUB_URL,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
hub.on('message', async (msg: Message) => {
|
|
233
|
+
try {
|
|
234
|
+
const response = await config.onMessage(msg, hub);
|
|
235
|
+
|
|
236
|
+
// If handler returns a response, send it back
|
|
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
|
+
}
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
hub.on('connected', () => {
|
|
253
|
+
console.log(`[GopherHole Agent] Connected as ${hub.agentId}`);
|
|
254
|
+
if (config.onConnect) {
|
|
255
|
+
config.onConnect(hub);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
hub.on('error', (err: Error) => {
|
|
260
|
+
if (config.onError) {
|
|
261
|
+
config.onError(err, hub);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
hub,
|
|
267
|
+
start: async () => {
|
|
268
|
+
await hub.connect();
|
|
269
|
+
},
|
|
270
|
+
stop: () => {
|
|
271
|
+
hub.disconnect();
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export default GopherHole;
|
package/test.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { GopherHole } from './dist/index.js';
|
|
2
|
+
|
|
3
|
+
const NOVA_KEY = 'gph_ddf4842ab273455b9719d6224ccbf170';
|
|
4
|
+
const MARKETCLAW_KEY = 'gph_d6be074540c84db191c5aaed6464233b';
|
|
5
|
+
|
|
6
|
+
async function test() {
|
|
7
|
+
console.log('šæļø Testing GopherHole connection...\n');
|
|
8
|
+
|
|
9
|
+
let novaReceived = false;
|
|
10
|
+
let marketclawReceived = false;
|
|
11
|
+
|
|
12
|
+
// Connect Nova
|
|
13
|
+
const nova = new GopherHole(NOVA_KEY, { reconnect: false });
|
|
14
|
+
|
|
15
|
+
nova.on('message', (msg) => {
|
|
16
|
+
console.log(`šØ Nova received: "${msg.payload.parts[0]?.text}" from ${msg.from}`);
|
|
17
|
+
novaReceived = true;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await nova.connect();
|
|
22
|
+
console.log(`ā
Nova connected as ${nova.agentId}`);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error('ā Nova connection failed:', err);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Connect MarketClaw
|
|
29
|
+
const marketclaw = new GopherHole(MARKETCLAW_KEY, { reconnect: false });
|
|
30
|
+
|
|
31
|
+
marketclaw.on('message', (msg) => {
|
|
32
|
+
console.log(`šØ MarketClaw received: "${msg.payload.parts[0]?.text}" from ${msg.from}`);
|
|
33
|
+
marketclawReceived = true;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
await marketclaw.connect();
|
|
38
|
+
console.log(`ā
MarketClaw connected as ${marketclaw.agentId}`);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error('ā MarketClaw connection failed:', err);
|
|
41
|
+
nova.disconnect();
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Test messaging
|
|
46
|
+
console.log('\nš¤ Nova sending message to MarketClaw...');
|
|
47
|
+
try {
|
|
48
|
+
const result = await nova.sendText(marketclaw.agentId!, 'Hello MarketClaw! This is Nova via GopherHole šæļø');
|
|
49
|
+
console.log('ā
Message sent:', result.id);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error('ā Send failed:', err);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// MarketClaw replies
|
|
55
|
+
console.log('š¤ MarketClaw sending reply to Nova...');
|
|
56
|
+
try {
|
|
57
|
+
const result = await marketclaw.sendText(nova.agentId!, 'Hey Nova! MarketClaw here. GopherHole is working! š');
|
|
58
|
+
console.log('ā
Reply sent:', result.id);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error('ā Reply failed:', err);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Wait for both messages to be delivered
|
|
64
|
+
console.log('\nā³ Waiting for message delivery via queue...');
|
|
65
|
+
const start = Date.now();
|
|
66
|
+
while ((!novaReceived || !marketclawReceived) && Date.now() - start < 15000) {
|
|
67
|
+
await new Promise(r => setTimeout(r, 500));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Results
|
|
71
|
+
console.log('\nš Results:');
|
|
72
|
+
console.log(` Nova received MarketClaw's reply: ${novaReceived ? 'ā
' : 'ā'}`);
|
|
73
|
+
console.log(` MarketClaw received Nova's message: ${marketclawReceived ? 'ā
' : 'ā'}`);
|
|
74
|
+
|
|
75
|
+
// Cleanup
|
|
76
|
+
nova.disconnect();
|
|
77
|
+
marketclaw.disconnect();
|
|
78
|
+
|
|
79
|
+
if (novaReceived && marketclawReceived) {
|
|
80
|
+
console.log('\nš SUCCESS! Both agents communicated via GopherHole!');
|
|
81
|
+
} else {
|
|
82
|
+
console.log('\nā ļø Partial success - some messages may still be in queue');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test().catch(console.error);
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|