gopherhole_openclaw_a2a 0.3.15 → 0.4.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/SKILL.md CHANGED
@@ -16,7 +16,7 @@ Add to your Clawdbot config:
16
16
  channels:
17
17
  a2a:
18
18
  enabled: true
19
- agentId: nova # Our identifier (default: clawdbot)
19
+ agentId: nova # Our identifier (default: openclaw)
20
20
  agentName: Nova # Display name
21
21
  bridgeUrl: ws://localhost:8080/a2a # A2A bridge/hub (optional)
22
22
  agents: # Direct agent connections (optional)
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * A2A Channel Plugin Entry Point
3
- * Enables Clawdbot to communicate with other AI agents via A2A protocol
3
+ * Enables OpenClaw to communicate with other AI agents via A2A protocol
4
4
  */
5
5
  import { getA2AConnectionManager } from './src/channel.js';
6
- interface ClawdbotPluginApi {
6
+ interface OpenClawPluginApi {
7
7
  runtime: unknown;
8
8
  registerChannel(opts: {
9
9
  plugin: unknown;
@@ -29,7 +29,7 @@ declare const plugin: {
29
29
  additionalProperties: boolean;
30
30
  properties: {};
31
31
  };
32
- register(api: ClawdbotPluginApi): void;
32
+ register(api: OpenClawPluginApi): void;
33
33
  };
34
34
  export default plugin;
35
35
  export { getA2AConnectionManager };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * A2A Channel Plugin Entry Point
3
- * Enables Clawdbot to communicate with other AI agents via A2A protocol
3
+ * Enables OpenClaw to communicate with other AI agents via A2A protocol
4
4
  */
5
5
  import { a2aPlugin, setA2ARuntime, getA2AConnectionManager } from './src/channel.js';
6
6
  import { readFileSync } from 'fs';
@@ -17,14 +17,24 @@ let pendingChats = new Map(); // keyed by runId
17
17
  let connected = false;
18
18
  let handshakeComplete = false;
19
19
  function getGatewayToken() {
20
- try {
21
- const configPath = join(homedir(), '.clawdbot', 'clawdbot.json');
22
- const config = JSON.parse(readFileSync(configPath, 'utf8'));
23
- return config?.gateway?.auth?.token ?? null;
24
- }
25
- catch {
26
- return null;
20
+ // Try the current OpenClaw config location first, then the legacy
21
+ // Clawdbot path as a fallback for users still on older installs.
22
+ const candidates = [
23
+ join(homedir(), '.openclaw', 'openclaw.json'),
24
+ join(homedir(), '.clawdbot', 'clawdbot.json'),
25
+ ];
26
+ for (const configPath of candidates) {
27
+ try {
28
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
29
+ const token = config?.gateway?.auth?.token;
30
+ if (token)
31
+ return token;
32
+ }
33
+ catch {
34
+ // file missing or unparseable — try next
35
+ }
27
36
  }
37
+ return null;
28
38
  }
29
39
  export async function connectToGateway(port = 18789) {
30
40
  if (ws && connected && handshakeComplete)
@@ -5,7 +5,7 @@
5
5
  import { appendFileSync, mkdirSync, existsSync } from 'fs';
6
6
  import { join } from 'path';
7
7
  import { homedir } from 'os';
8
- const LOG_DIR = join(homedir(), '.clawdbot', 'logs');
8
+ const LOG_DIR = join(homedir(), '.openclaw', 'logs');
9
9
  const A2A_LOG_FILE = join(LOG_DIR, 'a2a.log');
10
10
  // Ensure log directory exists
11
11
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gopherhole_openclaw_a2a",
3
- "version": "0.3.15",
3
+ "version": "0.4.1",
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",
@@ -12,14 +12,13 @@
12
12
  "test:watch": "vitest",
13
13
  "prepublishOnly": "npm run build"
14
14
  },
15
- "clawdbot": {
15
+ "openclaw": {
16
16
  "extensions": [
17
17
  "./dist/index.js"
18
18
  ]
19
19
  },
20
20
  "keywords": [
21
21
  "openclaw",
22
- "clawdbot",
23
22
  "a2a",
24
23
  "gopherhole",
25
24
  "agent",
@@ -27,13 +26,16 @@
27
26
  ],
28
27
  "repository": {
29
28
  "type": "git",
30
- "url": "https://github.com/helixdata/gopherhole.git",
31
- "directory": "clawdbot-plugin"
29
+ "url": "git+https://github.com/helixdata/gopherhole-clients.git",
30
+ "directory": "packages/plugin-openclaw"
32
31
  },
33
32
  "homepage": "https://gopherhole.ai",
33
+ "bugs": {
34
+ "url": "https://github.com/helixdata/gopherhole-clients/issues"
35
+ },
34
36
  "license": "MIT",
35
37
  "dependencies": {
36
- "@gopherhole/sdk": "^0.5.0",
38
+ "@gopherhole/sdk": "^0.7.1",
37
39
  "uuid": "^10.0.0"
38
40
  },
39
41
  "devDependencies": {
@@ -44,5 +46,11 @@
44
46
  },
45
47
  "peerDependencies": {
46
48
  "openclaw": "*"
47
- }
49
+ },
50
+ "files": [
51
+ "dist",
52
+ "openclaw.plugin.json",
53
+ "README.md",
54
+ "SKILL.md"
55
+ ]
48
56
  }
package/A2A-FIX-GUIDE.md DELETED
@@ -1,122 +0,0 @@
1
- # OpenClaw A2A Plugin - Response Relay Fix Guide
2
-
3
- ## Problem
4
- When Agent A sends to Agent B via GopherHole, Agent B can receive and process the message, but the response doesn't get relayed back. Agent A sees their original message echoed instead of the actual response.
5
-
6
- ## Root Cause Analysis
7
-
8
- ### 1. TaskId Flow Issue
9
- The `taskId` is critical for routing responses back. If it's missing or invalid:
10
- - `connection.ts` generates a fake `gph-<timestamp>` ID
11
- - `respond()` sends to this fake task
12
- - GopherHole ignores it (task doesn't exist)
13
- - `waitForTask` falls back to history, returning the original request
14
-
15
- ### 2. Agent ID Mismatch
16
- GopherHole validates that responses come from the correct agent:
17
- ```sql
18
- SELECT context_id FROM tasks WHERE id = ? AND server_agent_id = ?
19
- ```
20
- If the agent's connected ID doesn't match `server_agent_id`, the response is silently dropped.
21
-
22
- ## Debugging Steps
23
-
24
- ### Step 1: Add Logging to connection.ts
25
-
26
- In `handleIncomingMessage()`:
27
- ```typescript
28
- private handleIncomingMessage(message: Message): void {
29
- console.log(`[a2a] RAW incoming message:`, JSON.stringify(message, null, 2));
30
-
31
- if (!this.messageHandler) return;
32
-
33
- console.log(`[a2a] Received message from ${message.from}, taskId=${message.taskId}`);
34
- // ... rest of handler
35
- }
36
- ```
37
-
38
- In `sendResponseViaGopherHole()`:
39
- ```typescript
40
- sendResponseViaGopherHole(
41
- _targetAgentId: string,
42
- taskId: string,
43
- text: string,
44
- _contextId?: string
45
- ): void {
46
- console.log(`[a2a] Attempting respond - taskId=${taskId}, connected=${this.connected}, text=${text.slice(0, 100)}`);
47
-
48
- if (!taskId || taskId.startsWith('gph-')) {
49
- console.error(`[a2a] WARNING: Invalid taskId "${taskId}" - response will be lost!`);
50
- }
51
- // ... rest of method
52
- }
53
- ```
54
-
55
- ### Step 2: Verify Agent ID Configuration
56
-
57
- Check your config:
58
- ```yaml
59
- channels:
60
- a2a:
61
- enabled: true
62
- agentId: "agent-XXXXXXXX" # Must match your GopherHole agent ID
63
- apiKey: "gph_..."
64
- ```
65
-
66
- The `agentId` here should match exactly what's in your GopherHole dashboard.
67
-
68
- ### Step 3: Check SDK Message Event
69
-
70
- In the SDK (`@gopherhole/sdk`), the message handler should receive taskId:
71
- ```typescript
72
- this.emit('message', {
73
- from: data.from,
74
- taskId: data.taskId, // Must be present!
75
- payload: data.payload,
76
- timestamp: data.timestamp || Date.now(),
77
- });
78
- ```
79
-
80
- If `data.taskId` is undefined in the raw WebSocket message from GopherHole, that's a server-side bug.
81
-
82
- ## The Fix
83
-
84
- ### Option A: Ensure taskId is propagated (SDK/Server fix)
85
-
86
- The GopherHole hub's `deliverMessage` should always include taskId:
87
- ```typescript
88
- conn.ws.send(JSON.stringify({
89
- type: 'message',
90
- from: message.from,
91
- taskId: message.taskId, // This must be present
92
- payload: message.payload,
93
- }));
94
- ```
95
-
96
- ### Option B: Plugin resilience (Client-side workaround)
97
-
98
- If taskId isn't available, the plugin could store a mapping:
99
- ```typescript
100
- // In handleIncomingMessage:
101
- const taskId = message.taskId || `pending-${message.from}-${Date.now()}`;
102
- if (!message.taskId) {
103
- // Store for later - need server-side support for this
104
- console.warn('[a2a] No taskId in message - response routing may fail');
105
- }
106
- ```
107
-
108
- ## Testing
109
-
110
- 1. Send a simple message to your agent via GopherHole
111
- 2. Check logs for:
112
- - `[a2a] RAW incoming message:` - does it have taskId?
113
- - `[a2a] Attempting respond - taskId=` - is taskId valid?
114
- 3. If taskId is missing/invalid, the issue is upstream (SDK or GopherHole server)
115
-
116
- ## Quick Checklist
117
-
118
- - [ ] Agent ID in config matches GopherHole dashboard
119
- - [ ] API key is valid and has correct permissions
120
- - [ ] WebSocket connection is established (check for "Connected to GopherHole Hub via SDK" log)
121
- - [ ] Incoming messages have valid taskId (not undefined or gph-*)
122
- - [ ] Agent's `respond()` is actually being called after processing
package/index.ts DELETED
@@ -1,232 +0,0 @@
1
- /**
2
- * A2A Channel Plugin Entry Point
3
- * Enables Clawdbot to communicate with other AI agents via A2A protocol
4
- */
5
-
6
- import { a2aPlugin, setA2ARuntime, getA2AConnectionManager } from './src/channel.js';
7
- import { readFileSync } from 'fs';
8
- import { basename, extname } from 'path';
9
-
10
- // Minimal plugin interface
11
- interface ClawdbotPluginApi {
12
- runtime: unknown;
13
- registerChannel(opts: { plugin: unknown }): void;
14
- registerTool?(opts: {
15
- name: string;
16
- description: string;
17
- parameters: unknown;
18
- execute: (id: string, params: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text: string }> }>;
19
- }): void;
20
- }
21
-
22
- const plugin = {
23
- id: 'gopherhole_openclaw_a2a',
24
- name: 'A2A Protocol',
25
- description: 'Agent-to-Agent communication channel',
26
- configSchema: { type: 'object', additionalProperties: false, properties: {} },
27
- register(api: ClawdbotPluginApi) {
28
- setA2ARuntime(api.runtime);
29
- api.registerChannel({ plugin: a2aPlugin });
30
-
31
- // Register a tool for interacting with connected agents
32
- api.registerTool?.({
33
- name: 'a2a_agents',
34
- description: 'List connected A2A agents and send messages to them',
35
- parameters: {
36
- type: 'object',
37
- properties: {
38
- action: {
39
- type: 'string',
40
- enum: ['list', 'send'],
41
- description: 'Action to perform',
42
- },
43
- agentId: {
44
- type: 'string',
45
- description: 'Target agent ID (for send action)',
46
- },
47
- message: {
48
- type: 'string',
49
- description: 'Text message to send (for send action)',
50
- },
51
- image: {
52
- type: 'string',
53
- description: 'Path to image file to send (for send action)',
54
- },
55
- file: {
56
- type: 'string',
57
- description: 'Path to file to send - PDF, documents, etc. (for send action)',
58
- },
59
- },
60
- required: ['action'],
61
- },
62
- execute: async (_id, params) => {
63
- const action = params.action as string;
64
- const agentId = params.agentId as string | undefined;
65
- const message = params.message as string | undefined;
66
- const imagePath = params.image as string | undefined;
67
- const filePath = params.file as string | undefined;
68
-
69
- const manager = getA2AConnectionManager();
70
- if (!manager) {
71
- return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: 'A2A channel not running' }) }] };
72
- }
73
-
74
- if (action === 'list') {
75
- const hubStatus = manager.listAgents();
76
- const availableAgents = await manager.listAvailableAgents();
77
- return { content: [{ type: 'text', text: JSON.stringify({
78
- status: 'ok',
79
- connected: hubStatus.some(h => h.connected),
80
- agents: availableAgents
81
- }) }] };
82
- }
83
-
84
- if (action === 'send') {
85
- if (!agentId || (!message && !imagePath && !filePath)) {
86
- return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: 'agentId and (message, image, or file) required for send action' }) }] };
87
- }
88
- try {
89
- const isGopherHoleConnected = manager.isGopherHoleConnected();
90
- const isDirectConnection = manager.isConnected(agentId) && agentId !== 'gopherhole';
91
-
92
- // Build parts array
93
- const parts: Array<{ kind: string; text?: string; data?: string; mimeType?: string }> = [];
94
-
95
- // Add text part if message provided
96
- if (message) {
97
- parts.push({ kind: 'text', text: message });
98
- }
99
-
100
- // Add file part if image or file path provided
101
- const attachmentPath = imagePath || filePath;
102
- if (attachmentPath) {
103
- try {
104
- const fileData = readFileSync(attachmentPath);
105
- const base64Data = fileData.toString('base64');
106
- const ext = extname(attachmentPath).toLowerCase();
107
- const mimeTypes: Record<string, string> = {
108
- // Images
109
- '.png': 'image/png',
110
- '.jpg': 'image/jpeg',
111
- '.jpeg': 'image/jpeg',
112
- '.gif': 'image/gif',
113
- '.webp': 'image/webp',
114
- '.svg': 'image/svg+xml',
115
- // Documents
116
- '.pdf': 'application/pdf',
117
- '.doc': 'application/msword',
118
- '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
119
- '.xls': 'application/vnd.ms-excel',
120
- '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
121
- '.ppt': 'application/vnd.ms-powerpoint',
122
- '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
123
- '.txt': 'text/plain',
124
- '.csv': 'text/csv',
125
- '.json': 'application/json',
126
- '.xml': 'application/xml',
127
- '.html': 'text/html',
128
- '.md': 'text/markdown',
129
- // Archives
130
- '.zip': 'application/zip',
131
- };
132
- const mimeType = mimeTypes[ext] || 'application/octet-stream';
133
- parts.push({ kind: 'data', data: base64Data, mimeType });
134
- } catch (fileErr) {
135
- return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: `Failed to read file: ${(fileErr as Error).message}` }) }] };
136
- }
137
- }
138
-
139
- let response;
140
- if (isDirectConnection) {
141
- // Direct WebSocket - only supports text for now
142
- if (message) {
143
- response = await manager.sendMessage(agentId, message);
144
- } else {
145
- return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: 'Direct connections only support text messages' }) }] };
146
- }
147
- } else if (isGopherHoleConnected) {
148
- // Route through GopherHole hub with multi-part support
149
- response = await manager.sendPartsViaGopherHole(agentId, parts);
150
- } else {
151
- return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: `Cannot reach agent ${agentId} - no direct connection or GopherHole` }) }] };
152
- }
153
- return { content: [{ type: 'text', text: JSON.stringify({ status: 'ok', agentId, response }) }] };
154
- } catch (err) {
155
- return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: (err as Error).message }) }] };
156
- }
157
- }
158
-
159
- return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: `Unknown action: ${action}` }) }] };
160
- },
161
- });
162
-
163
- // Register a tool for location-based agent discovery
164
- api.registerTool?.({
165
- name: 'a2a_discover_nearby',
166
- description: 'Find A2A agents near a geographic location',
167
- parameters: {
168
- type: 'object',
169
- properties: {
170
- lat: {
171
- type: 'number',
172
- description: 'Latitude of search center',
173
- },
174
- lng: {
175
- type: 'number',
176
- description: 'Longitude of search center',
177
- },
178
- radius: {
179
- type: 'number',
180
- description: 'Search radius in kilometers (default: 10, max: 500)',
181
- },
182
- tag: {
183
- type: 'string',
184
- description: 'Filter by tag (e.g., "retail", "food")',
185
- },
186
- category: {
187
- type: 'string',
188
- description: 'Filter by category',
189
- },
190
- limit: {
191
- type: 'number',
192
- description: 'Maximum number of results (default: 20, max: 50)',
193
- },
194
- },
195
- required: ['lat', 'lng'],
196
- },
197
- execute: async (_id, params) => {
198
- const lat = params.lat as number;
199
- const lng = params.lng as number;
200
- const radius = params.radius as number | undefined;
201
- const tag = params.tag as string | undefined;
202
- const category = params.category as string | undefined;
203
- const limit = params.limit as number | undefined;
204
-
205
- const manager = getA2AConnectionManager();
206
- if (!manager) {
207
- return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: 'A2A channel not running' }) }] };
208
- }
209
-
210
- if (!manager.isGopherHoleConnected()) {
211
- return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: 'Not connected to GopherHole' }) }] };
212
- }
213
-
214
- try {
215
- const agents = await manager.discoverNearby({ lat, lng, radius, tag, category, limit });
216
- return { content: [{ type: 'text', text: JSON.stringify({
217
- status: 'ok',
218
- center: { lat, lng },
219
- radius: radius || 10,
220
- count: agents.length,
221
- agents
222
- }) }] };
223
- } catch (err) {
224
- return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: (err as Error).message }) }] };
225
- }
226
- },
227
- });
228
- },
229
- };
230
-
231
- export default plugin;
232
- export { getA2AConnectionManager };