@gopherhole/sdk 0.1.4 → 0.2.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/dist/index.d.mts CHANGED
@@ -97,13 +97,13 @@ interface A2AArtifact {
97
97
  lastChunk?: boolean;
98
98
  metadata?: Record<string, unknown>;
99
99
  }
100
- interface JsonRpcRequest {
100
+ interface JsonRpcRequest$1 {
101
101
  jsonrpc: '2.0';
102
102
  method: string;
103
103
  params?: Record<string, unknown>;
104
104
  id: string | number;
105
105
  }
106
- interface JsonRpcResponse<T = unknown> {
106
+ interface JsonRpcResponse$1<T = unknown> {
107
107
  jsonrpc: '2.0';
108
108
  result?: T;
109
109
  error?: JsonRpcError;
@@ -179,6 +179,116 @@ interface TaskArtifactUpdateEvent {
179
179
  }
180
180
  type TaskEvent = TaskStatusUpdateEvent | TaskArtifactUpdateEvent;
181
181
 
182
+ /**
183
+ * GopherHoleAgent - Helper for building webhook-based A2A agents
184
+ *
185
+ * Use this to create agents that receive requests from the GopherHole hub
186
+ * via webhooks (e.g., Cloudflare Workers, Express servers).
187
+ */
188
+
189
+ interface IncomingMessage {
190
+ role: 'user' | 'agent';
191
+ parts: AgentMessagePart[];
192
+ metadata?: Record<string, unknown>;
193
+ }
194
+ interface AgentMessagePart {
195
+ kind: 'text' | 'file' | 'data';
196
+ text?: string;
197
+ mimeType?: string;
198
+ data?: string;
199
+ uri?: string;
200
+ }
201
+ interface AgentTaskResult {
202
+ id: string;
203
+ contextId: string;
204
+ status: AgentTaskStatus;
205
+ messages: IncomingMessage[];
206
+ artifacts?: AgentArtifact[];
207
+ }
208
+ interface AgentTaskStatus {
209
+ state: 'submitted' | 'working' | 'input-required' | 'completed' | 'failed' | 'canceled';
210
+ timestamp: string;
211
+ message?: string;
212
+ }
213
+ interface AgentArtifact {
214
+ name?: string;
215
+ mimeType?: string;
216
+ parts?: AgentMessagePart[];
217
+ }
218
+ interface JsonRpcRequest {
219
+ jsonrpc: '2.0';
220
+ method: string;
221
+ params?: Record<string, unknown>;
222
+ id?: string | number;
223
+ }
224
+ interface JsonRpcResponse {
225
+ jsonrpc: '2.0';
226
+ result?: unknown;
227
+ error?: {
228
+ code: number;
229
+ message: string;
230
+ data?: unknown;
231
+ };
232
+ id?: string | number | null;
233
+ }
234
+ interface MessageContext {
235
+ /** The incoming message */
236
+ message: IncomingMessage;
237
+ /** Extracted text content from message parts */
238
+ text: string;
239
+ /** Task ID if provided */
240
+ taskId?: string;
241
+ /** Context ID if provided */
242
+ contextId?: string;
243
+ /** Full params from JSON-RPC request */
244
+ params: Record<string, unknown>;
245
+ }
246
+ type MessageHandler = (ctx: MessageContext) => Promise<string | AgentTaskResult> | string | AgentTaskResult;
247
+ interface GopherHoleAgentOptions {
248
+ /** Agent card for discovery */
249
+ card: AgentCard;
250
+ /** API key for authentication (from GopherHole hub) */
251
+ apiKey?: string;
252
+ /** Handler for incoming messages */
253
+ onMessage: MessageHandler;
254
+ }
255
+ declare class GopherHoleAgent {
256
+ private card;
257
+ private apiKey?;
258
+ private onMessage;
259
+ constructor(options: GopherHoleAgentOptions);
260
+ /** Get the agent card */
261
+ getCard(): AgentCard;
262
+ /** Verify authorization header */
263
+ verifyAuth(authHeader: string | null): boolean;
264
+ /**
265
+ * Handle an incoming HTTP request
266
+ * Returns a Response object (works with Cloudflare Workers, Bun, etc.)
267
+ */
268
+ handleRequest(request: Request): Promise<Response>;
269
+ /**
270
+ * Handle a JSON-RPC request directly
271
+ */
272
+ handleJsonRpc(req: JsonRpcRequest): Promise<JsonRpcResponse>;
273
+ private handleMessageSend;
274
+ /**
275
+ * Create a completed task result from a text response
276
+ */
277
+ createTaskResult(originalMessage: IncomingMessage, responseText: string, contextId?: string): AgentTaskResult;
278
+ /**
279
+ * Helper to create a text message part
280
+ */
281
+ static textPart(text: string): AgentMessagePart;
282
+ /**
283
+ * Helper to create a file message part
284
+ */
285
+ static filePart(uri: string, mimeType: string): AgentMessagePart;
286
+ /**
287
+ * Helper to create a data message part (base64)
288
+ */
289
+ static dataPart(data: string, mimeType: string): AgentMessagePart;
290
+ }
291
+
182
292
  interface GopherHoleOptions {
183
293
  /** API key (starts with gph_) */
184
294
  apiKey: string;
@@ -534,4 +644,4 @@ interface RatingResult {
534
644
  ratingCount: number;
535
645
  }
536
646
 
537
- export { type A2AArtifact, type A2AMessage, type A2ATask, type A2ATaskStatus, type AgentAuthentication, type AgentCapabilities, type AgentCard, type AgentCardConfig, type AgentCategory, type AgentInfoResult, type AgentReview, type AgentSkill, type AgentSkillConfig, type Artifact, type ContentMode, type DataContent, type DataPart, type DiscoverOptions, type DiscoverResult, type FileContent, type FilePart, GopherHole, type GopherHoleOptions, type InputMode, type JsonRpcError, JsonRpcErrorCodes, type JsonRpcRequest, type JsonRpcResponse, type Message, type MessagePart, type MessagePayload, type MessageSendConfiguration, type OutputMode, type Part, type PublicAgent, type PushNotificationConfig, type RatingResult, type SendAndWaitOptions, type SendOptions, type Task, type TaskArtifactUpdateEvent, type TaskEvent, type TaskListConfiguration, type TaskPushNotificationConfig, type TaskQueryConfiguration, type TaskState, type TaskStatus, type TaskStatusUpdateEvent, type TextPart, GopherHole as default, getTaskResponseText };
647
+ export { type A2AArtifact, type A2AMessage, type A2ATask, type A2ATaskStatus, type AgentArtifact, type AgentAuthentication, type AgentCapabilities, type AgentCard, type AgentCardConfig, type AgentCategory, type AgentInfoResult, type AgentMessagePart, type AgentReview, type AgentSkill, type AgentSkillConfig, type AgentTaskResult, type AgentTaskStatus, type Artifact, type ContentMode, type DataContent, type DataPart, type DiscoverOptions, type DiscoverResult, type FileContent, type FilePart, GopherHole, GopherHoleAgent, type GopherHoleAgentOptions, type GopherHoleOptions, type IncomingMessage, type InputMode, type JsonRpcError, JsonRpcErrorCodes, type JsonRpcRequest$1 as JsonRpcRequest, type JsonRpcResponse$1 as JsonRpcResponse, type Message, type MessageContext, type MessageHandler, type MessagePart, type MessagePayload, type MessageSendConfiguration, type OutputMode, type Part, type PublicAgent, type PushNotificationConfig, type RatingResult, type SendAndWaitOptions, type SendOptions, type Task, type TaskArtifactUpdateEvent, type TaskEvent, type TaskListConfiguration, type TaskPushNotificationConfig, type TaskQueryConfiguration, type AgentTaskResult as TaskResult, type TaskState, type TaskStatus, type TaskStatusUpdateEvent, type TextPart, GopherHole as default, getTaskResponseText };
package/dist/index.d.ts CHANGED
@@ -97,13 +97,13 @@ interface A2AArtifact {
97
97
  lastChunk?: boolean;
98
98
  metadata?: Record<string, unknown>;
99
99
  }
100
- interface JsonRpcRequest {
100
+ interface JsonRpcRequest$1 {
101
101
  jsonrpc: '2.0';
102
102
  method: string;
103
103
  params?: Record<string, unknown>;
104
104
  id: string | number;
105
105
  }
106
- interface JsonRpcResponse<T = unknown> {
106
+ interface JsonRpcResponse$1<T = unknown> {
107
107
  jsonrpc: '2.0';
108
108
  result?: T;
109
109
  error?: JsonRpcError;
@@ -179,6 +179,116 @@ interface TaskArtifactUpdateEvent {
179
179
  }
180
180
  type TaskEvent = TaskStatusUpdateEvent | TaskArtifactUpdateEvent;
181
181
 
182
+ /**
183
+ * GopherHoleAgent - Helper for building webhook-based A2A agents
184
+ *
185
+ * Use this to create agents that receive requests from the GopherHole hub
186
+ * via webhooks (e.g., Cloudflare Workers, Express servers).
187
+ */
188
+
189
+ interface IncomingMessage {
190
+ role: 'user' | 'agent';
191
+ parts: AgentMessagePart[];
192
+ metadata?: Record<string, unknown>;
193
+ }
194
+ interface AgentMessagePart {
195
+ kind: 'text' | 'file' | 'data';
196
+ text?: string;
197
+ mimeType?: string;
198
+ data?: string;
199
+ uri?: string;
200
+ }
201
+ interface AgentTaskResult {
202
+ id: string;
203
+ contextId: string;
204
+ status: AgentTaskStatus;
205
+ messages: IncomingMessage[];
206
+ artifacts?: AgentArtifact[];
207
+ }
208
+ interface AgentTaskStatus {
209
+ state: 'submitted' | 'working' | 'input-required' | 'completed' | 'failed' | 'canceled';
210
+ timestamp: string;
211
+ message?: string;
212
+ }
213
+ interface AgentArtifact {
214
+ name?: string;
215
+ mimeType?: string;
216
+ parts?: AgentMessagePart[];
217
+ }
218
+ interface JsonRpcRequest {
219
+ jsonrpc: '2.0';
220
+ method: string;
221
+ params?: Record<string, unknown>;
222
+ id?: string | number;
223
+ }
224
+ interface JsonRpcResponse {
225
+ jsonrpc: '2.0';
226
+ result?: unknown;
227
+ error?: {
228
+ code: number;
229
+ message: string;
230
+ data?: unknown;
231
+ };
232
+ id?: string | number | null;
233
+ }
234
+ interface MessageContext {
235
+ /** The incoming message */
236
+ message: IncomingMessage;
237
+ /** Extracted text content from message parts */
238
+ text: string;
239
+ /** Task ID if provided */
240
+ taskId?: string;
241
+ /** Context ID if provided */
242
+ contextId?: string;
243
+ /** Full params from JSON-RPC request */
244
+ params: Record<string, unknown>;
245
+ }
246
+ type MessageHandler = (ctx: MessageContext) => Promise<string | AgentTaskResult> | string | AgentTaskResult;
247
+ interface GopherHoleAgentOptions {
248
+ /** Agent card for discovery */
249
+ card: AgentCard;
250
+ /** API key for authentication (from GopherHole hub) */
251
+ apiKey?: string;
252
+ /** Handler for incoming messages */
253
+ onMessage: MessageHandler;
254
+ }
255
+ declare class GopherHoleAgent {
256
+ private card;
257
+ private apiKey?;
258
+ private onMessage;
259
+ constructor(options: GopherHoleAgentOptions);
260
+ /** Get the agent card */
261
+ getCard(): AgentCard;
262
+ /** Verify authorization header */
263
+ verifyAuth(authHeader: string | null): boolean;
264
+ /**
265
+ * Handle an incoming HTTP request
266
+ * Returns a Response object (works with Cloudflare Workers, Bun, etc.)
267
+ */
268
+ handleRequest(request: Request): Promise<Response>;
269
+ /**
270
+ * Handle a JSON-RPC request directly
271
+ */
272
+ handleJsonRpc(req: JsonRpcRequest): Promise<JsonRpcResponse>;
273
+ private handleMessageSend;
274
+ /**
275
+ * Create a completed task result from a text response
276
+ */
277
+ createTaskResult(originalMessage: IncomingMessage, responseText: string, contextId?: string): AgentTaskResult;
278
+ /**
279
+ * Helper to create a text message part
280
+ */
281
+ static textPart(text: string): AgentMessagePart;
282
+ /**
283
+ * Helper to create a file message part
284
+ */
285
+ static filePart(uri: string, mimeType: string): AgentMessagePart;
286
+ /**
287
+ * Helper to create a data message part (base64)
288
+ */
289
+ static dataPart(data: string, mimeType: string): AgentMessagePart;
290
+ }
291
+
182
292
  interface GopherHoleOptions {
183
293
  /** API key (starts with gph_) */
184
294
  apiKey: string;
@@ -534,4 +644,4 @@ interface RatingResult {
534
644
  ratingCount: number;
535
645
  }
536
646
 
537
- export { type A2AArtifact, type A2AMessage, type A2ATask, type A2ATaskStatus, type AgentAuthentication, type AgentCapabilities, type AgentCard, type AgentCardConfig, type AgentCategory, type AgentInfoResult, type AgentReview, type AgentSkill, type AgentSkillConfig, type Artifact, type ContentMode, type DataContent, type DataPart, type DiscoverOptions, type DiscoverResult, type FileContent, type FilePart, GopherHole, type GopherHoleOptions, type InputMode, type JsonRpcError, JsonRpcErrorCodes, type JsonRpcRequest, type JsonRpcResponse, type Message, type MessagePart, type MessagePayload, type MessageSendConfiguration, type OutputMode, type Part, type PublicAgent, type PushNotificationConfig, type RatingResult, type SendAndWaitOptions, type SendOptions, type Task, type TaskArtifactUpdateEvent, type TaskEvent, type TaskListConfiguration, type TaskPushNotificationConfig, type TaskQueryConfiguration, type TaskState, type TaskStatus, type TaskStatusUpdateEvent, type TextPart, GopherHole as default, getTaskResponseText };
647
+ export { type A2AArtifact, type A2AMessage, type A2ATask, type A2ATaskStatus, type AgentArtifact, type AgentAuthentication, type AgentCapabilities, type AgentCard, type AgentCardConfig, type AgentCategory, type AgentInfoResult, type AgentMessagePart, type AgentReview, type AgentSkill, type AgentSkillConfig, type AgentTaskResult, type AgentTaskStatus, type Artifact, type ContentMode, type DataContent, type DataPart, type DiscoverOptions, type DiscoverResult, type FileContent, type FilePart, GopherHole, GopherHoleAgent, type GopherHoleAgentOptions, type GopherHoleOptions, type IncomingMessage, type InputMode, type JsonRpcError, JsonRpcErrorCodes, type JsonRpcRequest$1 as JsonRpcRequest, type JsonRpcResponse$1 as JsonRpcResponse, type Message, type MessageContext, type MessageHandler, type MessagePart, type MessagePayload, type MessageSendConfiguration, type OutputMode, type Part, type PublicAgent, type PushNotificationConfig, type RatingResult, type SendAndWaitOptions, type SendOptions, type Task, type TaskArtifactUpdateEvent, type TaskEvent, type TaskListConfiguration, type TaskPushNotificationConfig, type TaskQueryConfiguration, type AgentTaskResult as TaskResult, type TaskState, type TaskStatus, type TaskStatusUpdateEvent, type TextPart, GopherHole as default, getTaskResponseText };
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  GopherHole: () => GopherHole,
24
+ GopherHoleAgent: () => GopherHoleAgent,
24
25
  JsonRpcErrorCodes: () => JsonRpcErrorCodes,
25
26
  default: () => index_default,
26
27
  getTaskResponseText: () => getTaskResponseText
@@ -44,6 +45,168 @@ var JsonRpcErrorCodes = {
44
45
  InvalidAgentCard: -32006
45
46
  };
46
47
 
48
+ // src/agent.ts
49
+ var GopherHoleAgent = class {
50
+ constructor(options) {
51
+ this.card = options.card;
52
+ this.apiKey = options.apiKey;
53
+ this.onMessage = options.onMessage;
54
+ }
55
+ /** Get the agent card */
56
+ getCard() {
57
+ return this.card;
58
+ }
59
+ /** Verify authorization header */
60
+ verifyAuth(authHeader) {
61
+ if (!this.apiKey) return true;
62
+ return authHeader === `Bearer ${this.apiKey}`;
63
+ }
64
+ /**
65
+ * Handle an incoming HTTP request
66
+ * Returns a Response object (works with Cloudflare Workers, Bun, etc.)
67
+ */
68
+ async handleRequest(request) {
69
+ const url = new URL(request.url);
70
+ const corsHeaders = {
71
+ "Access-Control-Allow-Origin": "*",
72
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
73
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
74
+ };
75
+ if (request.method === "OPTIONS") {
76
+ return new Response(null, { headers: corsHeaders });
77
+ }
78
+ if (url.pathname === "/.well-known/agent.json" || url.pathname === "/agent.json") {
79
+ return Response.json(this.card, { headers: corsHeaders });
80
+ }
81
+ if (url.pathname === "/health") {
82
+ return Response.json(
83
+ { status: "ok", agent: this.card.name, version: this.card.version || "1.0.0" },
84
+ { headers: corsHeaders }
85
+ );
86
+ }
87
+ if (request.method === "POST" && (url.pathname === "/" || url.pathname === "/a2a")) {
88
+ if (this.apiKey && !this.verifyAuth(request.headers.get("Authorization"))) {
89
+ return Response.json(
90
+ { jsonrpc: "2.0", error: { code: -32001, message: "Unauthorized" }, id: null },
91
+ { status: 401, headers: corsHeaders }
92
+ );
93
+ }
94
+ try {
95
+ const body = await request.json();
96
+ const response = await this.handleJsonRpc(body);
97
+ return Response.json(response, { headers: corsHeaders });
98
+ } catch {
99
+ return Response.json(
100
+ { jsonrpc: "2.0", error: { code: -32700, message: "Parse error" }, id: null },
101
+ { status: 400, headers: corsHeaders }
102
+ );
103
+ }
104
+ }
105
+ return new Response("Not Found", { status: 404, headers: corsHeaders });
106
+ }
107
+ /**
108
+ * Handle a JSON-RPC request directly
109
+ */
110
+ async handleJsonRpc(req) {
111
+ const { method, params = {}, id } = req;
112
+ switch (method) {
113
+ case "message/send":
114
+ return this.handleMessageSend(params, id);
115
+ case "tasks/get":
116
+ return {
117
+ jsonrpc: "2.0",
118
+ error: { code: -32601, message: "This agent does not support persistent tasks" },
119
+ id
120
+ };
121
+ case "tasks/cancel":
122
+ return {
123
+ jsonrpc: "2.0",
124
+ error: { code: -32601, message: "This agent does not support task cancellation" },
125
+ id
126
+ };
127
+ default:
128
+ return {
129
+ jsonrpc: "2.0",
130
+ error: { code: -32601, message: `Method not found: ${method}` },
131
+ id
132
+ };
133
+ }
134
+ }
135
+ async handleMessageSend(params, id) {
136
+ const message = params.message;
137
+ if (!message?.parts) {
138
+ return {
139
+ jsonrpc: "2.0",
140
+ error: { code: -32602, message: "Invalid params: message with parts required" },
141
+ id
142
+ };
143
+ }
144
+ const text = message.parts.filter((p) => p.kind === "text" || p.text).map((p) => p.text || "").join("\n").trim();
145
+ const config = params.configuration;
146
+ const ctx = {
147
+ message,
148
+ text,
149
+ taskId: params.taskId,
150
+ contextId: config?.contextId,
151
+ params
152
+ };
153
+ try {
154
+ const result = await this.onMessage(ctx);
155
+ if (typeof result === "string") {
156
+ const taskResult = this.createTaskResult(message, result, ctx.contextId);
157
+ return { jsonrpc: "2.0", result: taskResult, id };
158
+ }
159
+ return { jsonrpc: "2.0", result, id };
160
+ } catch (error) {
161
+ const errorMessage = error instanceof Error ? error.message : "Handler error";
162
+ return {
163
+ jsonrpc: "2.0",
164
+ error: { code: -32e3, message: errorMessage },
165
+ id
166
+ };
167
+ }
168
+ }
169
+ /**
170
+ * Create a completed task result from a text response
171
+ */
172
+ createTaskResult(originalMessage, responseText, contextId) {
173
+ return {
174
+ id: `task-${Date.now()}`,
175
+ contextId: contextId || `ctx-${Date.now()}`,
176
+ status: {
177
+ state: "completed",
178
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
179
+ },
180
+ messages: [
181
+ originalMessage,
182
+ {
183
+ role: "agent",
184
+ parts: [{ kind: "text", text: responseText }],
185
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString() }
186
+ }
187
+ ]
188
+ };
189
+ }
190
+ /**
191
+ * Helper to create a text message part
192
+ */
193
+ static textPart(text) {
194
+ return { kind: "text", text };
195
+ }
196
+ /**
197
+ * Helper to create a file message part
198
+ */
199
+ static filePart(uri, mimeType) {
200
+ return { kind: "file", uri, mimeType };
201
+ }
202
+ /**
203
+ * Helper to create a data message part (base64)
204
+ */
205
+ static dataPart(data, mimeType) {
206
+ return { kind: "data", data, mimeType };
207
+ }
208
+ };
209
+
47
210
  // src/index.ts
48
211
  function getTaskResponseText(task) {
49
212
  if (task.artifacts?.length) {
@@ -344,7 +507,7 @@ var GopherHole = class extends import_eventemitter3.EventEmitter {
344
507
  if (this.ws?.readyState === 1) {
345
508
  this.ws.send(JSON.stringify({ type: "ping" }));
346
509
  }
347
- }, 3e4);
510
+ }, 15e3);
348
511
  }
349
512
  /**
350
513
  * Stop ping interval
@@ -538,6 +701,7 @@ var index_default = GopherHole;
538
701
  // Annotate the CommonJS export names for ESM import in node:
539
702
  0 && (module.exports = {
540
703
  GopherHole,
704
+ GopherHoleAgent,
541
705
  JsonRpcErrorCodes,
542
706
  getTaskResponseText
543
707
  });
package/dist/index.mjs CHANGED
@@ -24,6 +24,168 @@ var JsonRpcErrorCodes = {
24
24
  InvalidAgentCard: -32006
25
25
  };
26
26
 
27
+ // src/agent.ts
28
+ var GopherHoleAgent = class {
29
+ constructor(options) {
30
+ this.card = options.card;
31
+ this.apiKey = options.apiKey;
32
+ this.onMessage = options.onMessage;
33
+ }
34
+ /** Get the agent card */
35
+ getCard() {
36
+ return this.card;
37
+ }
38
+ /** Verify authorization header */
39
+ verifyAuth(authHeader) {
40
+ if (!this.apiKey) return true;
41
+ return authHeader === `Bearer ${this.apiKey}`;
42
+ }
43
+ /**
44
+ * Handle an incoming HTTP request
45
+ * Returns a Response object (works with Cloudflare Workers, Bun, etc.)
46
+ */
47
+ async handleRequest(request) {
48
+ const url = new URL(request.url);
49
+ const corsHeaders = {
50
+ "Access-Control-Allow-Origin": "*",
51
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
52
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
53
+ };
54
+ if (request.method === "OPTIONS") {
55
+ return new Response(null, { headers: corsHeaders });
56
+ }
57
+ if (url.pathname === "/.well-known/agent.json" || url.pathname === "/agent.json") {
58
+ return Response.json(this.card, { headers: corsHeaders });
59
+ }
60
+ if (url.pathname === "/health") {
61
+ return Response.json(
62
+ { status: "ok", agent: this.card.name, version: this.card.version || "1.0.0" },
63
+ { headers: corsHeaders }
64
+ );
65
+ }
66
+ if (request.method === "POST" && (url.pathname === "/" || url.pathname === "/a2a")) {
67
+ if (this.apiKey && !this.verifyAuth(request.headers.get("Authorization"))) {
68
+ return Response.json(
69
+ { jsonrpc: "2.0", error: { code: -32001, message: "Unauthorized" }, id: null },
70
+ { status: 401, headers: corsHeaders }
71
+ );
72
+ }
73
+ try {
74
+ const body = await request.json();
75
+ const response = await this.handleJsonRpc(body);
76
+ return Response.json(response, { headers: corsHeaders });
77
+ } catch {
78
+ return Response.json(
79
+ { jsonrpc: "2.0", error: { code: -32700, message: "Parse error" }, id: null },
80
+ { status: 400, headers: corsHeaders }
81
+ );
82
+ }
83
+ }
84
+ return new Response("Not Found", { status: 404, headers: corsHeaders });
85
+ }
86
+ /**
87
+ * Handle a JSON-RPC request directly
88
+ */
89
+ async handleJsonRpc(req) {
90
+ const { method, params = {}, id } = req;
91
+ switch (method) {
92
+ case "message/send":
93
+ return this.handleMessageSend(params, id);
94
+ case "tasks/get":
95
+ return {
96
+ jsonrpc: "2.0",
97
+ error: { code: -32601, message: "This agent does not support persistent tasks" },
98
+ id
99
+ };
100
+ case "tasks/cancel":
101
+ return {
102
+ jsonrpc: "2.0",
103
+ error: { code: -32601, message: "This agent does not support task cancellation" },
104
+ id
105
+ };
106
+ default:
107
+ return {
108
+ jsonrpc: "2.0",
109
+ error: { code: -32601, message: `Method not found: ${method}` },
110
+ id
111
+ };
112
+ }
113
+ }
114
+ async handleMessageSend(params, id) {
115
+ const message = params.message;
116
+ if (!message?.parts) {
117
+ return {
118
+ jsonrpc: "2.0",
119
+ error: { code: -32602, message: "Invalid params: message with parts required" },
120
+ id
121
+ };
122
+ }
123
+ const text = message.parts.filter((p) => p.kind === "text" || p.text).map((p) => p.text || "").join("\n").trim();
124
+ const config = params.configuration;
125
+ const ctx = {
126
+ message,
127
+ text,
128
+ taskId: params.taskId,
129
+ contextId: config?.contextId,
130
+ params
131
+ };
132
+ try {
133
+ const result = await this.onMessage(ctx);
134
+ if (typeof result === "string") {
135
+ const taskResult = this.createTaskResult(message, result, ctx.contextId);
136
+ return { jsonrpc: "2.0", result: taskResult, id };
137
+ }
138
+ return { jsonrpc: "2.0", result, id };
139
+ } catch (error) {
140
+ const errorMessage = error instanceof Error ? error.message : "Handler error";
141
+ return {
142
+ jsonrpc: "2.0",
143
+ error: { code: -32e3, message: errorMessage },
144
+ id
145
+ };
146
+ }
147
+ }
148
+ /**
149
+ * Create a completed task result from a text response
150
+ */
151
+ createTaskResult(originalMessage, responseText, contextId) {
152
+ return {
153
+ id: `task-${Date.now()}`,
154
+ contextId: contextId || `ctx-${Date.now()}`,
155
+ status: {
156
+ state: "completed",
157
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
158
+ },
159
+ messages: [
160
+ originalMessage,
161
+ {
162
+ role: "agent",
163
+ parts: [{ kind: "text", text: responseText }],
164
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString() }
165
+ }
166
+ ]
167
+ };
168
+ }
169
+ /**
170
+ * Helper to create a text message part
171
+ */
172
+ static textPart(text) {
173
+ return { kind: "text", text };
174
+ }
175
+ /**
176
+ * Helper to create a file message part
177
+ */
178
+ static filePart(uri, mimeType) {
179
+ return { kind: "file", uri, mimeType };
180
+ }
181
+ /**
182
+ * Helper to create a data message part (base64)
183
+ */
184
+ static dataPart(data, mimeType) {
185
+ return { kind: "data", data, mimeType };
186
+ }
187
+ };
188
+
27
189
  // src/index.ts
28
190
  function getTaskResponseText(task) {
29
191
  if (task.artifacts?.length) {
@@ -324,7 +486,7 @@ var GopherHole = class extends EventEmitter {
324
486
  if (this.ws?.readyState === 1) {
325
487
  this.ws.send(JSON.stringify({ type: "ping" }));
326
488
  }
327
- }, 3e4);
489
+ }, 15e3);
328
490
  }
329
491
  /**
330
492
  * Stop ping interval
@@ -517,6 +679,7 @@ var GopherHole = class extends EventEmitter {
517
679
  var index_default = GopherHole;
518
680
  export {
519
681
  GopherHole,
682
+ GopherHoleAgent,
520
683
  JsonRpcErrorCodes,
521
684
  index_default as default,
522
685
  getTaskResponseText
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gopherhole/sdk",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "GopherHole SDK - Connect AI agents via the A2A protocol",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
package/src/agent.ts ADDED
@@ -0,0 +1,305 @@
1
+ /**
2
+ * GopherHoleAgent - Helper for building webhook-based A2A agents
3
+ *
4
+ * Use this to create agents that receive requests from the GopherHole hub
5
+ * via webhooks (e.g., Cloudflare Workers, Express servers).
6
+ */
7
+
8
+ import type { AgentCard, AgentSkill } from './types';
9
+
10
+ // Re-export types for convenience
11
+ export type { AgentCard, AgentSkill };
12
+
13
+ // ============================================================
14
+ // TYPES
15
+ // ============================================================
16
+
17
+ export interface IncomingMessage {
18
+ role: 'user' | 'agent';
19
+ parts: AgentMessagePart[];
20
+ metadata?: Record<string, unknown>;
21
+ }
22
+
23
+ export interface AgentMessagePart {
24
+ kind: 'text' | 'file' | 'data';
25
+ text?: string;
26
+ mimeType?: string;
27
+ data?: string;
28
+ uri?: string;
29
+ }
30
+
31
+ export interface AgentTaskResult {
32
+ id: string;
33
+ contextId: string;
34
+ status: AgentTaskStatus;
35
+ messages: IncomingMessage[];
36
+ artifacts?: AgentArtifact[];
37
+ }
38
+
39
+ export interface AgentTaskStatus {
40
+ state: 'submitted' | 'working' | 'input-required' | 'completed' | 'failed' | 'canceled';
41
+ timestamp: string;
42
+ message?: string;
43
+ }
44
+
45
+ export interface AgentArtifact {
46
+ name?: string;
47
+ mimeType?: string;
48
+ parts?: AgentMessagePart[];
49
+ }
50
+
51
+ interface JsonRpcRequest {
52
+ jsonrpc: '2.0';
53
+ method: string;
54
+ params?: Record<string, unknown>;
55
+ id?: string | number;
56
+ }
57
+
58
+ interface JsonRpcResponse {
59
+ jsonrpc: '2.0';
60
+ result?: unknown;
61
+ error?: { code: number; message: string; data?: unknown };
62
+ id?: string | number | null;
63
+ }
64
+
65
+ // ============================================================
66
+ // MESSAGE HANDLER TYPE
67
+ // ============================================================
68
+
69
+ export interface MessageContext {
70
+ /** The incoming message */
71
+ message: IncomingMessage;
72
+ /** Extracted text content from message parts */
73
+ text: string;
74
+ /** Task ID if provided */
75
+ taskId?: string;
76
+ /** Context ID if provided */
77
+ contextId?: string;
78
+ /** Full params from JSON-RPC request */
79
+ params: Record<string, unknown>;
80
+ }
81
+
82
+ export type MessageHandler = (ctx: MessageContext) => Promise<string | AgentTaskResult> | string | AgentTaskResult;
83
+
84
+ // ============================================================
85
+ // AGENT CLASS
86
+ // ============================================================
87
+
88
+ export interface GopherHoleAgentOptions {
89
+ /** Agent card for discovery */
90
+ card: AgentCard;
91
+ /** API key for authentication (from GopherHole hub) */
92
+ apiKey?: string;
93
+ /** Handler for incoming messages */
94
+ onMessage: MessageHandler;
95
+ }
96
+
97
+ export class GopherHoleAgent {
98
+ private card: AgentCard;
99
+ private apiKey?: string;
100
+ private onMessage: MessageHandler;
101
+
102
+ constructor(options: GopherHoleAgentOptions) {
103
+ this.card = options.card;
104
+ this.apiKey = options.apiKey;
105
+ this.onMessage = options.onMessage;
106
+ }
107
+
108
+ /** Get the agent card */
109
+ getCard(): AgentCard {
110
+ return this.card;
111
+ }
112
+
113
+ /** Verify authorization header */
114
+ verifyAuth(authHeader: string | null): boolean {
115
+ if (!this.apiKey) return true; // No auth configured
116
+ return authHeader === `Bearer ${this.apiKey}`;
117
+ }
118
+
119
+ /**
120
+ * Handle an incoming HTTP request
121
+ * Returns a Response object (works with Cloudflare Workers, Bun, etc.)
122
+ */
123
+ async handleRequest(request: Request): Promise<Response> {
124
+ const url = new URL(request.url);
125
+ const corsHeaders = {
126
+ 'Access-Control-Allow-Origin': '*',
127
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
128
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
129
+ };
130
+
131
+ // CORS preflight
132
+ if (request.method === 'OPTIONS') {
133
+ return new Response(null, { headers: corsHeaders });
134
+ }
135
+
136
+ // Agent card endpoints
137
+ if (url.pathname === '/.well-known/agent.json' || url.pathname === '/agent.json') {
138
+ return Response.json(this.card, { headers: corsHeaders });
139
+ }
140
+
141
+ // Health check
142
+ if (url.pathname === '/health') {
143
+ return Response.json(
144
+ { status: 'ok', agent: this.card.name, version: this.card.version || '1.0.0' },
145
+ { headers: corsHeaders }
146
+ );
147
+ }
148
+
149
+ // JSON-RPC endpoint
150
+ if (request.method === 'POST' && (url.pathname === '/' || url.pathname === '/a2a')) {
151
+ // Verify auth
152
+ if (this.apiKey && !this.verifyAuth(request.headers.get('Authorization'))) {
153
+ return Response.json(
154
+ { jsonrpc: '2.0', error: { code: -32001, message: 'Unauthorized' }, id: null },
155
+ { status: 401, headers: corsHeaders }
156
+ );
157
+ }
158
+
159
+ try {
160
+ const body = await request.json() as JsonRpcRequest;
161
+ const response = await this.handleJsonRpc(body);
162
+ return Response.json(response, { headers: corsHeaders });
163
+ } catch {
164
+ return Response.json(
165
+ { jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' }, id: null },
166
+ { status: 400, headers: corsHeaders }
167
+ );
168
+ }
169
+ }
170
+
171
+ return new Response('Not Found', { status: 404, headers: corsHeaders });
172
+ }
173
+
174
+ /**
175
+ * Handle a JSON-RPC request directly
176
+ */
177
+ async handleJsonRpc(req: JsonRpcRequest): Promise<JsonRpcResponse> {
178
+ const { method, params = {}, id } = req;
179
+
180
+ switch (method) {
181
+ case 'message/send':
182
+ return this.handleMessageSend(params, id);
183
+
184
+ case 'tasks/get':
185
+ return {
186
+ jsonrpc: '2.0',
187
+ error: { code: -32601, message: 'This agent does not support persistent tasks' },
188
+ id,
189
+ };
190
+
191
+ case 'tasks/cancel':
192
+ return {
193
+ jsonrpc: '2.0',
194
+ error: { code: -32601, message: 'This agent does not support task cancellation' },
195
+ id,
196
+ };
197
+
198
+ default:
199
+ return {
200
+ jsonrpc: '2.0',
201
+ error: { code: -32601, message: `Method not found: ${method}` },
202
+ id,
203
+ };
204
+ }
205
+ }
206
+
207
+ private async handleMessageSend(
208
+ params: Record<string, unknown>,
209
+ id?: string | number
210
+ ): Promise<JsonRpcResponse> {
211
+ const message = params.message as IncomingMessage | undefined;
212
+
213
+ if (!message?.parts) {
214
+ return {
215
+ jsonrpc: '2.0',
216
+ error: { code: -32602, message: 'Invalid params: message with parts required' },
217
+ id,
218
+ };
219
+ }
220
+
221
+ // Extract text from message parts
222
+ const text = message.parts
223
+ .filter(p => p.kind === 'text' || p.text)
224
+ .map(p => p.text || '')
225
+ .join('\n')
226
+ .trim();
227
+
228
+ const config = params.configuration as Record<string, unknown> | undefined;
229
+ const ctx: MessageContext = {
230
+ message,
231
+ text,
232
+ taskId: params.taskId as string | undefined,
233
+ contextId: config?.contextId as string | undefined,
234
+ params,
235
+ };
236
+
237
+ try {
238
+ const result = await this.onMessage(ctx);
239
+
240
+ // If handler returned a string, wrap it in a task result
241
+ if (typeof result === 'string') {
242
+ const taskResult = this.createTaskResult(message, result, ctx.contextId);
243
+ return { jsonrpc: '2.0', result: taskResult, id };
244
+ }
245
+
246
+ return { jsonrpc: '2.0', result, id };
247
+ } catch (error) {
248
+ const errorMessage = error instanceof Error ? error.message : 'Handler error';
249
+ return {
250
+ jsonrpc: '2.0',
251
+ error: { code: -32000, message: errorMessage },
252
+ id,
253
+ };
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Create a completed task result from a text response
259
+ */
260
+ createTaskResult(
261
+ originalMessage: IncomingMessage,
262
+ responseText: string,
263
+ contextId?: string
264
+ ): AgentTaskResult {
265
+ return {
266
+ id: `task-${Date.now()}`,
267
+ contextId: contextId || `ctx-${Date.now()}`,
268
+ status: {
269
+ state: 'completed',
270
+ timestamp: new Date().toISOString(),
271
+ },
272
+ messages: [
273
+ originalMessage,
274
+ {
275
+ role: 'agent',
276
+ parts: [{ kind: 'text', text: responseText }],
277
+ metadata: { generatedAt: new Date().toISOString() },
278
+ },
279
+ ],
280
+ };
281
+ }
282
+
283
+ /**
284
+ * Helper to create a text message part
285
+ */
286
+ static textPart(text: string): AgentMessagePart {
287
+ return { kind: 'text', text };
288
+ }
289
+
290
+ /**
291
+ * Helper to create a file message part
292
+ */
293
+ static filePart(uri: string, mimeType: string): AgentMessagePart {
294
+ return { kind: 'file', uri, mimeType };
295
+ }
296
+
297
+ /**
298
+ * Helper to create a data message part (base64)
299
+ */
300
+ static dataPart(data: string, mimeType: string): AgentMessagePart {
301
+ return { kind: 'data', data, mimeType };
302
+ }
303
+ }
304
+
305
+ export default GopherHoleAgent;
package/src/index.ts CHANGED
@@ -3,6 +3,22 @@ import { EventEmitter } from 'eventemitter3';
3
3
  // Re-export types
4
4
  export * from './types';
5
5
 
6
+ // Re-export agent helper for webhook-based agents
7
+ export {
8
+ GopherHoleAgent,
9
+ GopherHoleAgentOptions,
10
+ IncomingMessage,
11
+ AgentMessagePart,
12
+ AgentTaskResult,
13
+ AgentTaskStatus,
14
+ AgentArtifact,
15
+ MessageContext,
16
+ MessageHandler,
17
+ } from './agent';
18
+
19
+ // Convenience type aliases
20
+ export type { AgentTaskResult as TaskResult } from './agent';
21
+
6
22
  export interface GopherHoleOptions {
7
23
  /** API key (starts with gph_) */
8
24
  apiKey: string;
@@ -480,7 +496,7 @@ export class GopherHole extends EventEmitter<EventMap> {
480
496
  if (this.ws?.readyState === 1) { // OPEN
481
497
  this.ws.send(JSON.stringify({ type: 'ping' }));
482
498
  }
483
- }, 30000);
499
+ }, 15000); // 15s ping to keep connection alive
484
500
  }
485
501
 
486
502
  /**