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 +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/src/gateway-client.js +17 -7
- package/dist/src/logger.js +1 -1
- package/package.json +15 -7
- package/A2A-FIX-GUIDE.md +0 -122
- package/index.ts +0 -232
- package/src/channel.ts +0 -385
- package/src/connection.test.ts +0 -298
- package/src/connection.ts +0 -533
- package/src/gateway-client.ts +0 -328
- package/src/logger.ts +0 -118
- package/src/types.ts +0 -75
- package/test-image.mjs +0 -29
- package/test-image2.mjs +0 -37
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -9
- /package/{clawdbot.plugin.json → openclaw.plugin.json} +0 -0
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:
|
|
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
|
|
3
|
+
* Enables OpenClaw to communicate with other AI agents via A2A protocol
|
|
4
4
|
*/
|
|
5
5
|
import { getA2AConnectionManager } from './src/channel.js';
|
|
6
|
-
interface
|
|
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:
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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)
|
package/dist/src/logger.js
CHANGED
|
@@ -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(), '.
|
|
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
|
+
"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
|
-
"
|
|
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": "
|
|
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.
|
|
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 };
|