gopherhole_openclaw_a2a 0.2.7 → 0.3.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 CHANGED
@@ -10,15 +10,13 @@ openclaw plugins install gopherhole_openclaw_a2a
10
10
 
11
11
  Then add to your OpenClaw config (`~/.openclaw/openclaw.json`):
12
12
 
13
- ```json5
13
+ ```json
14
14
  {
15
- channels: {
16
- a2a: {
17
- bridgeUrl: "wss://gopherhole.ai/ws",
18
- gopherhole: {
19
- enabled: true,
20
- apiKey: "gph_your_api_key_here"
21
- }
15
+ "channels": {
16
+ "a2a": {
17
+ "enabled": true,
18
+ "bridgeUrl": "wss://gopherhole.ai/ws",
19
+ "apiKey": "gph_your_api_key_here"
22
20
  }
23
21
  }
24
22
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * A2A Channel Plugin for Clawdbot
2
+ * A2A Channel Plugin for OpenClaw
3
3
  * Enables communication with other AI agents via A2A protocol
4
4
  */
5
5
  import { A2AConnectionManager } from './connection.js';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * A2A Channel Plugin for Clawdbot
2
+ * A2A Channel Plugin for OpenClaw
3
3
  * Enables communication with other AI agents via A2A protocol
4
4
  */
5
5
  // Use minimal type imports - mostly self-contained
@@ -25,10 +25,9 @@ function resolveA2AAccount(opts) {
25
25
  accountId,
26
26
  name: config.agentName ?? 'A2A',
27
27
  enabled: config.enabled ?? false,
28
- configured: !!(config.bridgeUrl || (config.agents && config.agents.length > 0) || config.gopherhole?.enabled),
29
- agentId: config.agentId ?? 'clawdbot',
28
+ configured: !!(config.bridgeUrl && config.apiKey),
29
+ agentId: config.agentId ?? 'openclaw',
30
30
  bridgeUrl: config.bridgeUrl ?? null,
31
- agents: config.agents ?? [],
32
31
  config,
33
32
  };
34
33
  }
@@ -39,9 +38,9 @@ const meta = {
39
38
  detailLabel: 'A2A Protocol',
40
39
  docsPath: '/channels/a2a',
41
40
  docsLabel: 'a2a',
42
- blurb: 'Communicate with other AI agents via A2A protocol.',
41
+ blurb: 'Communicate with other AI agents via GopherHole A2A protocol.',
43
42
  systemImage: 'bubble.left.and.bubble.right',
44
- aliases: ['agent2agent'],
43
+ aliases: ['agent2agent', 'gopherhole'],
45
44
  order: 200,
46
45
  };
47
46
  export const a2aPlugin = {
@@ -98,8 +97,8 @@ export const a2aPlugin = {
98
97
  messaging: {
99
98
  normalizeTarget: (target) => target?.trim() ?? '',
100
99
  targetResolver: {
101
- looksLikeId: (id) => /^[a-z0-9_-]+$/i.test(id),
102
- hint: '<agentId>',
100
+ looksLikeId: (id) => /^[a-z0-9_@-]+$/i.test(id),
101
+ hint: '<agentId> (e.g. @memory, @echo)',
103
102
  },
104
103
  formatTargetDisplay: ({ target }) => target ?? '',
105
104
  },
@@ -108,7 +107,7 @@ export const a2aPlugin = {
108
107
  applyAccountName: ({ cfg }) => cfg,
109
108
  validateInput: ({ input }) => {
110
109
  if (!input.httpUrl && !input.customArgs) {
111
- return 'A2A requires --http-url (bridge URL) or agents configured in config.';
110
+ return 'A2A requires --http-url (bridge URL) or bridgeUrl + apiKey in config.';
112
111
  }
113
112
  return null;
114
113
  },
@@ -211,7 +210,7 @@ export const a2aPlugin = {
211
210
  .join('\n') ?? '';
212
211
  if (!text)
213
212
  return;
214
- // Route to Clawdbot's reply pipeline via gateway JSON-RPC
213
+ // Route to OpenClaw's reply pipeline via gateway JSON-RPC
215
214
  try {
216
215
  ctx.log?.info(`[a2a] Routing message from ${message.from}: "${text.slice(0, 100)}..."`);
217
216
  // Use chat.send to route the message through the agent
@@ -219,26 +218,14 @@ export const a2aPlugin = {
219
218
  const sessionKey = `agent:main:a2a:${message.from}`;
220
219
  const response = await sendChatMessage(sessionKey, text);
221
220
  ctx.log?.info(`[a2a] chat.send returned: ${response ? `text=${response.text?.slice(0, 50)}...` : 'null'}`);
222
- // Send response back to the agent
221
+ // Send response back to the agent via GopherHole
223
222
  if (response?.text) {
224
- // If message came via GopherHole, route response back through it
225
- if (agentId === 'gopherhole' && message.from) {
226
- connectionManager?.sendResponseViaGopherHole(message.from, message.taskId, response.text, message.contextId);
227
- }
228
- else {
229
- connectionManager?.sendResponse(agentId, message.taskId, response.text, message.contextId);
230
- }
223
+ connectionManager?.sendResponseViaGopherHole(message.from, message.taskId, response.text, message.contextId);
231
224
  }
232
225
  }
233
226
  catch (err) {
234
227
  ctx.log?.error(`[a2a] Error handling message:`, err);
235
- // If message came via GopherHole, route error back through it
236
- if (agentId === 'gopherhole' && message.from) {
237
- connectionManager?.sendResponseViaGopherHole(message.from, message.taskId, `Error: ${err.message}`, message.contextId);
238
- }
239
- else {
240
- connectionManager?.sendResponse(agentId, message.taskId, `Error: ${err.message}`, message.contextId);
241
- }
228
+ connectionManager?.sendResponseViaGopherHole(message.from, message.taskId, `Error: ${err.message}`, message.contextId);
242
229
  }
243
230
  }
244
231
  });
@@ -11,30 +11,29 @@ export class A2AConnectionManager {
11
11
  connected = false;
12
12
  constructor(config) {
13
13
  this.config = config;
14
- this.agentId = config.agentId ?? 'clawdbot';
14
+ this.agentId = config.agentId ?? 'openclaw';
15
15
  }
16
16
  setMessageHandler(handler) {
17
17
  this.messageHandler = handler;
18
18
  }
19
19
  async start() {
20
- // Connect to GopherHole if configured
21
- if (this.config.gopherhole?.enabled && this.config.gopherhole?.apiKey) {
20
+ // Connect to GopherHole if configured (flat config: enabled + apiKey)
21
+ if (this.config.enabled && this.config.apiKey) {
22
22
  await this.connectToGopherHole();
23
23
  }
24
24
  }
25
25
  async connectToGopherHole() {
26
- const gphConfig = this.config.gopherhole;
27
- const hubUrl = gphConfig.hubUrl || 'wss://gopherhole.ai/ws';
26
+ const hubUrl = this.config.bridgeUrl || 'wss://gopherhole.ai/ws';
28
27
  const timeoutMs = this.config.requestTimeoutMs ?? 180000;
29
28
  this.gopherhole = new GopherHole({
30
- apiKey: gphConfig.apiKey,
29
+ apiKey: this.config.apiKey,
31
30
  hubUrl,
32
31
  autoReconnect: true,
33
32
  reconnectDelay: this.config.reconnectIntervalMs ?? 5000,
34
33
  maxReconnectAttempts: 20,
35
34
  requestTimeout: timeoutMs,
36
35
  messageTimeout: timeoutMs,
37
- agentCard: gphConfig.agentCard ?? {
36
+ agentCard: this.config.agentCard ?? {
38
37
  name: this.config.agentName ?? 'OpenClaw',
39
38
  description: 'Personal AI assistant with tools, web search, browser control, and various skills',
40
39
  version: '0.1.0',
@@ -39,26 +39,27 @@ export interface A2AResponse {
39
39
  status: string;
40
40
  from?: string;
41
41
  }
42
+ /**
43
+ * A2A Channel Config (flat structure)
44
+ *
45
+ * Example:
46
+ * {
47
+ * "channels": {
48
+ * "a2a": {
49
+ * "enabled": true,
50
+ * "bridgeUrl": "wss://gopherhole.ai/ws",
51
+ * "apiKey": "gph_your_api_key"
52
+ * }
53
+ * }
54
+ * }
55
+ */
42
56
  export interface A2AChannelConfig {
43
57
  enabled?: boolean;
58
+ bridgeUrl?: string;
59
+ apiKey?: string;
44
60
  agentId?: string;
45
61
  agentName?: string;
46
- bridgeUrl?: string;
47
- agents?: Array<{
48
- id: string;
49
- url: string;
50
- name?: string;
51
- }>;
52
- gopherhole?: {
53
- enabled?: boolean;
54
- apiKey: string;
55
- hubUrl?: string;
56
- requestTimeoutMs?: number;
57
- agentCard?: A2AAgentCard;
58
- };
59
- auth?: {
60
- token?: string;
61
- };
62
+ agentCard?: A2AAgentCard;
62
63
  reconnectIntervalMs?: number;
63
64
  requestTimeoutMs?: number;
64
65
  }
@@ -69,10 +70,5 @@ export interface ResolvedA2AAccount {
69
70
  configured: boolean;
70
71
  agentId: string;
71
72
  bridgeUrl: string | null;
72
- agents: Array<{
73
- id: string;
74
- url: string;
75
- name?: string;
76
- }>;
77
73
  config: A2AChannelConfig;
78
74
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gopherhole_openclaw_a2a",
3
- "version": "0.2.7",
3
+ "version": "0.3.0",
4
4
  "description": "GopherHole A2A plugin for OpenClaw - connect your AI agent to the GopherHole network",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/channel.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * A2A Channel Plugin for Clawdbot
2
+ * A2A Channel Plugin for OpenClaw
3
3
  * Enables communication with other AI agents via A2A protocol
4
4
  */
5
5
 
@@ -18,7 +18,7 @@ import type {
18
18
  } from './types.js';
19
19
 
20
20
  // Minimal runtime interface - what we actually need
21
- interface ClawdbotRuntime {
21
+ interface OpenClawRuntime {
22
22
  handleInbound(params: {
23
23
  channel: string;
24
24
  chatId: string;
@@ -32,14 +32,14 @@ interface ClawdbotRuntime {
32
32
 
33
33
  // Runtime state
34
34
  let connectionManager: A2AConnectionManager | null = null;
35
- let currentRuntime: ClawdbotRuntime | null = null;
35
+ let currentRuntime: OpenClawRuntime | null = null;
36
36
 
37
37
  export function setA2ARuntime(runtime: unknown): void {
38
- currentRuntime = runtime as ClawdbotRuntime;
38
+ currentRuntime = runtime as OpenClawRuntime;
39
39
  }
40
40
 
41
41
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
- type ClawdbotConfig = any;
42
+ type OpenClawConfig = any;
43
43
 
44
44
  // Minimal channel plugin interfaces (self-contained)
45
45
  interface ChannelAccountSnapshot {
@@ -85,12 +85,12 @@ type ChannelPlugin<T = any> = {
85
85
  };
86
86
  };
87
87
 
88
- function resolveA2AConfig(cfg: ClawdbotConfig): A2AChannelConfig {
88
+ function resolveA2AConfig(cfg: OpenClawConfig): A2AChannelConfig {
89
89
  return cfg?.channels?.a2a ?? {};
90
90
  }
91
91
 
92
92
  function resolveA2AAccount(opts: {
93
- cfg: ClawdbotConfig;
93
+ cfg: OpenClawConfig;
94
94
  accountId?: string;
95
95
  }): ResolvedA2AAccount {
96
96
  const config = resolveA2AConfig(opts.cfg);
@@ -100,10 +100,9 @@ function resolveA2AAccount(opts: {
100
100
  accountId,
101
101
  name: config.agentName ?? 'A2A',
102
102
  enabled: config.enabled ?? false,
103
- configured: !!(config.bridgeUrl || (config.agents && config.agents.length > 0) || config.gopherhole?.enabled),
104
- agentId: config.agentId ?? 'clawdbot',
103
+ configured: !!(config.bridgeUrl && config.apiKey),
104
+ agentId: config.agentId ?? 'openclaw',
105
105
  bridgeUrl: config.bridgeUrl ?? null,
106
- agents: config.agents ?? [],
107
106
  config,
108
107
  };
109
108
  }
@@ -115,9 +114,9 @@ const meta = {
115
114
  detailLabel: 'A2A Protocol',
116
115
  docsPath: '/channels/a2a',
117
116
  docsLabel: 'a2a',
118
- blurb: 'Communicate with other AI agents via A2A protocol.',
117
+ blurb: 'Communicate with other AI agents via GopherHole A2A protocol.',
119
118
  systemImage: 'bubble.left.and.bubble.right',
120
- aliases: ['agent2agent'],
119
+ aliases: ['agent2agent', 'gopherhole'],
121
120
  order: 200,
122
121
  };
123
122
 
@@ -136,10 +135,10 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
136
135
  config: {
137
136
  listAccountIds: () => [DEFAULT_ACCOUNT_ID],
138
137
  resolveAccount: (cfg, accountId) =>
139
- resolveA2AAccount({ cfg: cfg as ClawdbotConfig, accountId }),
138
+ resolveA2AAccount({ cfg: cfg as OpenClawConfig, accountId }),
140
139
  defaultAccountId: () => DEFAULT_ACCOUNT_ID,
141
140
  setAccountEnabled: ({ cfg, enabled }) => {
142
- const next = cfg as ClawdbotConfig;
141
+ const next = cfg as OpenClawConfig;
143
142
  return {
144
143
  ...next,
145
144
  channels: {
@@ -149,9 +148,9 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
149
148
  enabled,
150
149
  },
151
150
  },
152
- } as ClawdbotConfig;
151
+ } as OpenClawConfig;
153
152
  },
154
- deleteAccount: ({ cfg }) => cfg as ClawdbotConfig,
153
+ deleteAccount: ({ cfg }) => cfg as OpenClawConfig,
155
154
  isConfigured: (account) => account.configured,
156
155
  describeAccount: (account): ChannelAccountSnapshot => ({
157
156
  accountId: account.accountId,
@@ -176,22 +175,22 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
176
175
  messaging: {
177
176
  normalizeTarget: (target) => target?.trim() ?? '',
178
177
  targetResolver: {
179
- looksLikeId: (id) => /^[a-z0-9_-]+$/i.test(id),
180
- hint: '<agentId>',
178
+ looksLikeId: (id) => /^[a-z0-9_@-]+$/i.test(id),
179
+ hint: '<agentId> (e.g. @memory, @echo)',
181
180
  },
182
181
  formatTargetDisplay: ({ target }) => target ?? '',
183
182
  },
184
183
  setup: {
185
184
  resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
186
- applyAccountName: ({ cfg }) => cfg as ClawdbotConfig,
185
+ applyAccountName: ({ cfg }) => cfg as OpenClawConfig,
187
186
  validateInput: ({ input }) => {
188
187
  if (!input.httpUrl && !input.customArgs) {
189
- return 'A2A requires --http-url (bridge URL) or agents configured in config.';
188
+ return 'A2A requires --http-url (bridge URL) or bridgeUrl + apiKey in config.';
190
189
  }
191
190
  return null;
192
191
  },
193
192
  applyAccountConfig: ({ cfg, input }) => {
194
- const next = cfg as ClawdbotConfig;
193
+ const next = cfg as OpenClawConfig;
195
194
  return {
196
195
  ...next,
197
196
  channels: {
@@ -202,7 +201,7 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
202
201
  ...(input.httpUrl ? { bridgeUrl: input.httpUrl } : {}),
203
202
  },
204
203
  },
205
- } as ClawdbotConfig;
204
+ } as OpenClawConfig;
206
205
  },
207
206
  },
208
207
  outbound: {
@@ -292,7 +291,7 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
292
291
 
293
292
  if (!text) return;
294
293
 
295
- // Route to Clawdbot's reply pipeline via gateway JSON-RPC
294
+ // Route to OpenClaw's reply pipeline via gateway JSON-RPC
296
295
  try {
297
296
  ctx.log?.info(`[a2a] Routing message from ${message.from}: "${text.slice(0, 100)}..."`);
298
297
 
@@ -303,43 +302,23 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
303
302
 
304
303
  ctx.log?.info(`[a2a] chat.send returned: ${response ? `text=${response.text?.slice(0, 50)}...` : 'null'}`);
305
304
 
306
- // Send response back to the agent
305
+ // Send response back to the agent via GopherHole
307
306
  if (response?.text) {
308
- // If message came via GopherHole, route response back through it
309
- if (agentId === 'gopherhole' && message.from) {
310
- connectionManager?.sendResponseViaGopherHole(
311
- message.from,
312
- message.taskId,
313
- response.text,
314
- message.contextId
315
- );
316
- } else {
317
- connectionManager?.sendResponse(
318
- agentId,
319
- message.taskId,
320
- response.text,
321
- message.contextId
322
- );
323
- }
324
- }
325
- } catch (err) {
326
- ctx.log?.error(`[a2a] Error handling message:`, err);
327
- // If message came via GopherHole, route error back through it
328
- if (agentId === 'gopherhole' && message.from) {
329
307
  connectionManager?.sendResponseViaGopherHole(
330
308
  message.from,
331
309
  message.taskId,
332
- `Error: ${(err as Error).message}`,
333
- message.contextId
334
- );
335
- } else {
336
- connectionManager?.sendResponse(
337
- agentId,
338
- message.taskId,
339
- `Error: ${(err as Error).message}`,
310
+ response.text,
340
311
  message.contextId
341
312
  );
342
313
  }
314
+ } catch (err) {
315
+ ctx.log?.error(`[a2a] Error handling message:`, err);
316
+ connectionManager?.sendResponseViaGopherHole(
317
+ message.from,
318
+ message.taskId,
319
+ `Error: ${(err as Error).message}`,
320
+ message.contextId
321
+ );
343
322
  }
344
323
  }
345
324
  });
package/src/connection.ts CHANGED
@@ -22,7 +22,7 @@ export class A2AConnectionManager {
22
22
 
23
23
  constructor(config: A2AChannelConfig) {
24
24
  this.config = config;
25
- this.agentId = config.agentId ?? 'clawdbot';
25
+ this.agentId = config.agentId ?? 'openclaw';
26
26
  }
27
27
 
28
28
  setMessageHandler(handler: MessageHandler): void {
@@ -30,26 +30,25 @@ export class A2AConnectionManager {
30
30
  }
31
31
 
32
32
  async start(): Promise<void> {
33
- // Connect to GopherHole if configured
34
- if (this.config.gopherhole?.enabled && this.config.gopherhole?.apiKey) {
33
+ // Connect to GopherHole if configured (flat config: enabled + apiKey)
34
+ if (this.config.enabled && this.config.apiKey) {
35
35
  await this.connectToGopherHole();
36
36
  }
37
37
  }
38
38
 
39
39
  private async connectToGopherHole(): Promise<void> {
40
- const gphConfig = this.config.gopherhole!;
41
- const hubUrl = gphConfig.hubUrl || 'wss://gopherhole.ai/ws';
40
+ const hubUrl = this.config.bridgeUrl || 'wss://gopherhole.ai/ws';
42
41
  const timeoutMs = this.config.requestTimeoutMs ?? 180000;
43
42
 
44
43
  this.gopherhole = new GopherHole({
45
- apiKey: gphConfig.apiKey,
44
+ apiKey: this.config.apiKey!,
46
45
  hubUrl,
47
46
  autoReconnect: true,
48
47
  reconnectDelay: this.config.reconnectIntervalMs ?? 5000,
49
48
  maxReconnectAttempts: 20,
50
49
  requestTimeout: timeoutMs,
51
50
  messageTimeout: timeoutMs,
52
- agentCard: gphConfig.agentCard ?? {
51
+ agentCard: this.config.agentCard ?? {
53
52
  name: this.config.agentName ?? 'OpenClaw',
54
53
  description: 'Personal AI assistant with tools, web search, browser control, and various skills',
55
54
  version: '0.1.0',
package/src/types.ts CHANGED
@@ -39,28 +39,29 @@ export interface A2AResponse {
39
39
  from?: string;
40
40
  }
41
41
 
42
+ /**
43
+ * A2A Channel Config (flat structure)
44
+ *
45
+ * Example:
46
+ * {
47
+ * "channels": {
48
+ * "a2a": {
49
+ * "enabled": true,
50
+ * "bridgeUrl": "wss://gopherhole.ai/ws",
51
+ * "apiKey": "gph_your_api_key"
52
+ * }
53
+ * }
54
+ * }
55
+ */
42
56
  export interface A2AChannelConfig {
43
57
  enabled?: boolean;
44
- agentId?: string; // Our agent ID (default: "clawdbot")
45
- agentName?: string; // Display name
46
- bridgeUrl?: string; // Legacy: direct bridge URL (ws://...)
47
- agents?: Array<{ // Legacy: direct agent connections
48
- id: string;
49
- url: string;
50
- name?: string;
51
- }>;
52
- gopherhole?: { // GopherHole Agent Hub connection
53
- enabled?: boolean;
54
- apiKey: string;
55
- hubUrl?: string; // Default: wss://gopherhole.ai/ws
56
- requestTimeoutMs?: number;
57
- agentCard?: A2AAgentCard; // Custom agent card (overrides defaults)
58
- };
59
- auth?: {
60
- token?: string;
61
- };
62
- reconnectIntervalMs?: number;
63
- requestTimeoutMs?: number;
58
+ bridgeUrl?: string; // WebSocket URL (default: wss://gopherhole.ai/ws)
59
+ apiKey?: string; // GopherHole API key (gph_...)
60
+ agentId?: string; // Our agent ID (default: "openclaw")
61
+ agentName?: string; // Display name for agent card
62
+ agentCard?: A2AAgentCard; // Custom agent card (overrides defaults)
63
+ reconnectIntervalMs?: number; // Reconnect delay (default: 5000)
64
+ requestTimeoutMs?: number; // Request timeout (default: 180000)
64
65
  }
65
66
 
66
67
  export interface ResolvedA2AAccount {
@@ -70,6 +71,5 @@ export interface ResolvedA2AAccount {
70
71
  configured: boolean;
71
72
  agentId: string;
72
73
  bridgeUrl: string | null;
73
- agents: Array<{ id: string; url: string; name?: string }>;
74
74
  config: A2AChannelConfig;
75
75
  }