gopherhole_openclaw_a2a 0.3.12 → 0.3.14

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.js CHANGED
@@ -141,6 +141,69 @@ const plugin = {
141
141
  return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: `Unknown action: ${action}` }) }] };
142
142
  },
143
143
  });
144
+ // Register a tool for location-based agent discovery
145
+ api.registerTool?.({
146
+ name: 'a2a_discover_nearby',
147
+ description: 'Find A2A agents near a geographic location',
148
+ parameters: {
149
+ type: 'object',
150
+ properties: {
151
+ lat: {
152
+ type: 'number',
153
+ description: 'Latitude of search center',
154
+ },
155
+ lng: {
156
+ type: 'number',
157
+ description: 'Longitude of search center',
158
+ },
159
+ radius: {
160
+ type: 'number',
161
+ description: 'Search radius in kilometers (default: 10, max: 500)',
162
+ },
163
+ tag: {
164
+ type: 'string',
165
+ description: 'Filter by tag (e.g., "retail", "food")',
166
+ },
167
+ category: {
168
+ type: 'string',
169
+ description: 'Filter by category',
170
+ },
171
+ limit: {
172
+ type: 'number',
173
+ description: 'Maximum number of results (default: 20, max: 50)',
174
+ },
175
+ },
176
+ required: ['lat', 'lng'],
177
+ },
178
+ execute: async (_id, params) => {
179
+ const lat = params.lat;
180
+ const lng = params.lng;
181
+ const radius = params.radius;
182
+ const tag = params.tag;
183
+ const category = params.category;
184
+ const limit = params.limit;
185
+ const manager = getA2AConnectionManager();
186
+ if (!manager) {
187
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: 'A2A channel not running' }) }] };
188
+ }
189
+ if (!manager.isGopherHoleConnected()) {
190
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: 'Not connected to GopherHole' }) }] };
191
+ }
192
+ try {
193
+ const agents = await manager.discoverNearby({ lat, lng, radius, tag, category, limit });
194
+ return { content: [{ type: 'text', text: JSON.stringify({
195
+ status: 'ok',
196
+ center: { lat, lng },
197
+ radius: radius || 10,
198
+ count: agents.length,
199
+ agents
200
+ }) }] };
201
+ }
202
+ catch (err) {
203
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: err.message }) }] };
204
+ }
205
+ },
206
+ });
144
207
  },
145
208
  };
146
209
  export default plugin;
@@ -80,6 +80,7 @@ export declare class A2AConnectionManager {
80
80
  skillTag?: string;
81
81
  contentMode?: string;
82
82
  sort?: string;
83
+ owner?: string;
83
84
  verified?: boolean;
84
85
  limit?: number;
85
86
  offset?: number;
@@ -92,6 +93,32 @@ export declare class A2AConnectionManager {
92
93
  tenantName?: string;
93
94
  avgRating?: number;
94
95
  }>>;
96
+ /**
97
+ * Discover agents near a geographic location
98
+ */
99
+ discoverNearby(options: {
100
+ lat: number;
101
+ lng: number;
102
+ radius?: number;
103
+ tag?: string;
104
+ category?: string;
105
+ limit?: number;
106
+ offset?: number;
107
+ }): Promise<Array<{
108
+ id: string;
109
+ name: string;
110
+ description?: string;
111
+ verified?: boolean;
112
+ tenantName?: string;
113
+ avgRating?: number;
114
+ location?: {
115
+ name: string;
116
+ lat: number;
117
+ lng: number;
118
+ country: string;
119
+ };
120
+ distance?: number;
121
+ }>>;
95
122
  /**
96
123
  * List connection status (for backward compatibility)
97
124
  */
@@ -333,6 +333,13 @@ export class A2AConnectionManager {
333
333
  const result = await this.a2aRpc('x-gopherhole/agents.discover', options);
334
334
  return result?.agents || [];
335
335
  }
336
+ /**
337
+ * Discover agents near a geographic location
338
+ */
339
+ async discoverNearby(options) {
340
+ const result = await this.a2aRpc('x-gopherhole/agents.discover.nearby', options);
341
+ return result?.agents || [];
342
+ }
336
343
  /**
337
344
  * List connection status (for backward compatibility)
338
345
  */
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Unit tests for OpenClaw plugin discovery methods
3
+ * Tests the new discovery params: tag, skillTag, contentMode, sort, offset, scope
4
+ */
5
+ export {};
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Unit tests for OpenClaw plugin discovery methods
3
+ * Tests the new discovery params: tag, skillTag, contentMode, sort, offset, scope
4
+ */
5
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
6
+ import { A2AConnectionManager } from './connection';
7
+ // Mock fetch
8
+ const mockFetch = vi.fn();
9
+ global.fetch = mockFetch;
10
+ // Mock @gopherhole/sdk
11
+ vi.mock('@gopherhole/sdk', () => ({
12
+ GopherHole: vi.fn().mockImplementation(() => ({
13
+ connect: vi.fn(),
14
+ disconnect: vi.fn(),
15
+ on: vi.fn(),
16
+ connected: false,
17
+ id: null,
18
+ })),
19
+ getTaskResponseText: vi.fn(),
20
+ }));
21
+ describe('A2AConnectionManager Discovery', () => {
22
+ let manager;
23
+ const mockApiKey = 'gph_test_key';
24
+ beforeEach(() => {
25
+ manager = new A2AConnectionManager({
26
+ enabled: true,
27
+ apiKey: mockApiKey,
28
+ bridgeUrl: 'wss://test.gopherhole.ai/ws',
29
+ });
30
+ mockFetch.mockReset();
31
+ });
32
+ afterEach(() => {
33
+ vi.clearAllMocks();
34
+ });
35
+ const mockDiscoverResponse = {
36
+ agents: [
37
+ {
38
+ id: 'agent-1',
39
+ name: 'Test Agent',
40
+ description: 'A test agent',
41
+ verified: true,
42
+ tenantName: 'TestTenant',
43
+ avgRating: 4.5,
44
+ },
45
+ ],
46
+ };
47
+ const setupMockRpcResponse = (result) => {
48
+ mockFetch.mockResolvedValueOnce({
49
+ ok: true,
50
+ json: () => Promise.resolve({ result }),
51
+ });
52
+ };
53
+ describe('discoverAgents() with new params', () => {
54
+ it('should call discover with tag param', async () => {
55
+ setupMockRpcResponse(mockDiscoverResponse);
56
+ await manager.discoverAgents({ tag: 'ai' });
57
+ expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
58
+ method: 'POST',
59
+ body: expect.stringContaining('"tag":"ai"'),
60
+ }));
61
+ });
62
+ it('should call discover with skillTag param', async () => {
63
+ setupMockRpcResponse(mockDiscoverResponse);
64
+ await manager.discoverAgents({ skillTag: 'nlp' });
65
+ expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
66
+ body: expect.stringContaining('"skillTag":"nlp"'),
67
+ }));
68
+ });
69
+ it('should call discover with contentMode param', async () => {
70
+ setupMockRpcResponse(mockDiscoverResponse);
71
+ await manager.discoverAgents({ contentMode: 'text/markdown' });
72
+ expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
73
+ body: expect.stringContaining('"contentMode":"text/markdown"'),
74
+ }));
75
+ });
76
+ it('should call discover with sort param', async () => {
77
+ setupMockRpcResponse(mockDiscoverResponse);
78
+ await manager.discoverAgents({ sort: 'rating' });
79
+ expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
80
+ body: expect.stringContaining('"sort":"rating"'),
81
+ }));
82
+ });
83
+ it('should call discover with offset param', async () => {
84
+ setupMockRpcResponse(mockDiscoverResponse);
85
+ await manager.discoverAgents({ offset: 20 });
86
+ expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
87
+ body: expect.stringContaining('"offset":20'),
88
+ }));
89
+ });
90
+ it('should call discover with scope=tenant', async () => {
91
+ setupMockRpcResponse(mockDiscoverResponse);
92
+ await manager.discoverAgents({ scope: 'tenant' });
93
+ expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
94
+ body: expect.stringContaining('"scope":"tenant"'),
95
+ }));
96
+ });
97
+ });
98
+ describe('discoverAgents() with combined params', () => {
99
+ it('should combine multiple new params', async () => {
100
+ setupMockRpcResponse(mockDiscoverResponse);
101
+ await manager.discoverAgents({
102
+ tag: 'ai',
103
+ skillTag: 'nlp',
104
+ sort: 'popular',
105
+ offset: 10,
106
+ });
107
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
108
+ expect(body.params.tag).toBe('ai');
109
+ expect(body.params.skillTag).toBe('nlp');
110
+ expect(body.params.sort).toBe('popular');
111
+ expect(body.params.offset).toBe(10);
112
+ });
113
+ it('should combine query with new params', async () => {
114
+ setupMockRpcResponse(mockDiscoverResponse);
115
+ await manager.discoverAgents({
116
+ query: 'weather',
117
+ tag: 'api',
118
+ contentMode: 'application/json',
119
+ sort: 'recent',
120
+ limit: 25,
121
+ });
122
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
123
+ expect(body.params.query).toBe('weather');
124
+ expect(body.params.tag).toBe('api');
125
+ expect(body.params.contentMode).toBe('application/json');
126
+ expect(body.params.sort).toBe('recent');
127
+ expect(body.params.limit).toBe(25);
128
+ });
129
+ });
130
+ describe('discoverAgents() edge cases', () => {
131
+ it('should handle empty results', async () => {
132
+ setupMockRpcResponse({ agents: [] });
133
+ const result = await manager.discoverAgents({ tag: 'nonexistent' });
134
+ expect(result).toHaveLength(0);
135
+ });
136
+ it('should handle RPC errors gracefully', async () => {
137
+ mockFetch.mockResolvedValueOnce({
138
+ ok: true,
139
+ json: () => Promise.resolve({ error: { message: 'Not found' } }),
140
+ });
141
+ const result = await manager.discoverAgents({ tag: 'test' });
142
+ expect(result).toEqual([]);
143
+ });
144
+ it('should handle network errors', async () => {
145
+ mockFetch.mockRejectedValueOnce(new Error('Network error'));
146
+ const result = await manager.discoverAgents({ tag: 'test' });
147
+ expect(result).toEqual([]);
148
+ });
149
+ it('should return empty array when apiKey not configured', async () => {
150
+ const managerNoKey = new A2AConnectionManager({
151
+ enabled: true,
152
+ // No apiKey
153
+ });
154
+ const result = await managerNoKey.discoverAgents({ tag: 'test' });
155
+ expect(result).toEqual([]);
156
+ expect(mockFetch).not.toHaveBeenCalled();
157
+ });
158
+ });
159
+ describe('discoverAgents() sort values', () => {
160
+ it('should accept rating sort', async () => {
161
+ setupMockRpcResponse(mockDiscoverResponse);
162
+ await manager.discoverAgents({ sort: 'rating' });
163
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
164
+ expect(body.params.sort).toBe('rating');
165
+ });
166
+ it('should accept popular sort', async () => {
167
+ setupMockRpcResponse(mockDiscoverResponse);
168
+ await manager.discoverAgents({ sort: 'popular' });
169
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
170
+ expect(body.params.sort).toBe('popular');
171
+ });
172
+ it('should accept recent sort', async () => {
173
+ setupMockRpcResponse(mockDiscoverResponse);
174
+ await manager.discoverAgents({ sort: 'recent' });
175
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
176
+ expect(body.params.sort).toBe('recent');
177
+ });
178
+ });
179
+ describe('discoverAgents() response mapping', () => {
180
+ it('should map response correctly', async () => {
181
+ setupMockRpcResponse({
182
+ agents: [
183
+ {
184
+ id: 'agent-1',
185
+ name: 'Test Agent',
186
+ description: 'A test agent',
187
+ verified: true,
188
+ tenantName: 'TestTenant',
189
+ avgRating: 4.5,
190
+ },
191
+ ],
192
+ });
193
+ const result = await manager.discoverAgents({ query: 'test' });
194
+ expect(result).toEqual([
195
+ {
196
+ id: 'agent-1',
197
+ name: 'Test Agent',
198
+ description: 'A test agent',
199
+ verified: true,
200
+ tenantName: 'TestTenant',
201
+ avgRating: 4.5,
202
+ },
203
+ ]);
204
+ });
205
+ });
206
+ describe('listAvailableAgents()', () => {
207
+ it('should list available agents', async () => {
208
+ setupMockRpcResponse({
209
+ agents: [
210
+ {
211
+ id: 'agent-1',
212
+ name: 'Test Agent',
213
+ description: 'A test agent',
214
+ verified: true,
215
+ accessType: 'same-tenant',
216
+ },
217
+ ],
218
+ });
219
+ const result = await manager.listAvailableAgents();
220
+ expect(result).toHaveLength(1);
221
+ expect(result[0].accessType).toBe('same-tenant');
222
+ });
223
+ });
224
+ });
package/index.ts CHANGED
@@ -159,6 +159,72 @@ const plugin = {
159
159
  return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: `Unknown action: ${action}` }) }] };
160
160
  },
161
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
+ });
162
228
  },
163
229
  };
164
230
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gopherhole_openclaw_a2a",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
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",
@@ -8,6 +8,8 @@
8
8
  "scripts": {
9
9
  "build": "tsc",
10
10
  "clean": "rm -rf dist",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
11
13
  "prepublishOnly": "npm run build"
12
14
  },
13
15
  "clawdbot": {
@@ -35,8 +37,10 @@
35
37
  "uuid": "^10.0.0"
36
38
  },
37
39
  "devDependencies": {
40
+ "@types/node": "^25.5.0",
38
41
  "@types/uuid": "^10.0.0",
39
- "typescript": "^5.9.3"
42
+ "typescript": "^5.9.3",
43
+ "vitest": "^1.6.0"
40
44
  },
41
45
  "peerDependencies": {
42
46
  "openclaw": "*"
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Unit tests for OpenClaw plugin discovery methods
3
+ * Tests the new discovery params: tag, skillTag, contentMode, sort, offset, scope
4
+ */
5
+
6
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
7
+ import { A2AConnectionManager } from './connection';
8
+
9
+ // Mock fetch
10
+ const mockFetch = vi.fn();
11
+ global.fetch = mockFetch;
12
+
13
+ // Mock @gopherhole/sdk
14
+ vi.mock('@gopherhole/sdk', () => ({
15
+ GopherHole: vi.fn().mockImplementation(() => ({
16
+ connect: vi.fn(),
17
+ disconnect: vi.fn(),
18
+ on: vi.fn(),
19
+ connected: false,
20
+ id: null,
21
+ })),
22
+ getTaskResponseText: vi.fn(),
23
+ }));
24
+
25
+ describe('A2AConnectionManager Discovery', () => {
26
+ let manager: A2AConnectionManager;
27
+ const mockApiKey = 'gph_test_key';
28
+
29
+ beforeEach(() => {
30
+ manager = new A2AConnectionManager({
31
+ enabled: true,
32
+ apiKey: mockApiKey,
33
+ bridgeUrl: 'wss://test.gopherhole.ai/ws',
34
+ });
35
+ mockFetch.mockReset();
36
+ });
37
+
38
+ afterEach(() => {
39
+ vi.clearAllMocks();
40
+ });
41
+
42
+ const mockDiscoverResponse = {
43
+ agents: [
44
+ {
45
+ id: 'agent-1',
46
+ name: 'Test Agent',
47
+ description: 'A test agent',
48
+ verified: true,
49
+ tenantName: 'TestTenant',
50
+ avgRating: 4.5,
51
+ },
52
+ ],
53
+ };
54
+
55
+ const setupMockRpcResponse = (result: any) => {
56
+ mockFetch.mockResolvedValueOnce({
57
+ ok: true,
58
+ json: () => Promise.resolve({ result }),
59
+ });
60
+ };
61
+
62
+ describe('discoverAgents() with new params', () => {
63
+ it('should call discover with tag param', async () => {
64
+ setupMockRpcResponse(mockDiscoverResponse);
65
+
66
+ await manager.discoverAgents({ tag: 'ai' });
67
+
68
+ expect(mockFetch).toHaveBeenCalledWith(
69
+ expect.any(String),
70
+ expect.objectContaining({
71
+ method: 'POST',
72
+ body: expect.stringContaining('"tag":"ai"'),
73
+ })
74
+ );
75
+ });
76
+
77
+ it('should call discover with skillTag param', async () => {
78
+ setupMockRpcResponse(mockDiscoverResponse);
79
+
80
+ await manager.discoverAgents({ skillTag: 'nlp' });
81
+
82
+ expect(mockFetch).toHaveBeenCalledWith(
83
+ expect.any(String),
84
+ expect.objectContaining({
85
+ body: expect.stringContaining('"skillTag":"nlp"'),
86
+ })
87
+ );
88
+ });
89
+
90
+ it('should call discover with contentMode param', async () => {
91
+ setupMockRpcResponse(mockDiscoverResponse);
92
+
93
+ await manager.discoverAgents({ contentMode: 'text/markdown' });
94
+
95
+ expect(mockFetch).toHaveBeenCalledWith(
96
+ expect.any(String),
97
+ expect.objectContaining({
98
+ body: expect.stringContaining('"contentMode":"text/markdown"'),
99
+ })
100
+ );
101
+ });
102
+
103
+ it('should call discover with sort param', async () => {
104
+ setupMockRpcResponse(mockDiscoverResponse);
105
+
106
+ await manager.discoverAgents({ sort: 'rating' });
107
+
108
+ expect(mockFetch).toHaveBeenCalledWith(
109
+ expect.any(String),
110
+ expect.objectContaining({
111
+ body: expect.stringContaining('"sort":"rating"'),
112
+ })
113
+ );
114
+ });
115
+
116
+ it('should call discover with offset param', async () => {
117
+ setupMockRpcResponse(mockDiscoverResponse);
118
+
119
+ await manager.discoverAgents({ offset: 20 });
120
+
121
+ expect(mockFetch).toHaveBeenCalledWith(
122
+ expect.any(String),
123
+ expect.objectContaining({
124
+ body: expect.stringContaining('"offset":20'),
125
+ })
126
+ );
127
+ });
128
+
129
+ it('should call discover with scope=tenant', async () => {
130
+ setupMockRpcResponse(mockDiscoverResponse);
131
+
132
+ await manager.discoverAgents({ scope: 'tenant' });
133
+
134
+ expect(mockFetch).toHaveBeenCalledWith(
135
+ expect.any(String),
136
+ expect.objectContaining({
137
+ body: expect.stringContaining('"scope":"tenant"'),
138
+ })
139
+ );
140
+ });
141
+ });
142
+
143
+ describe('discoverAgents() with combined params', () => {
144
+ it('should combine multiple new params', async () => {
145
+ setupMockRpcResponse(mockDiscoverResponse);
146
+
147
+ await manager.discoverAgents({
148
+ tag: 'ai',
149
+ skillTag: 'nlp',
150
+ sort: 'popular',
151
+ offset: 10,
152
+ });
153
+
154
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
155
+ expect(body.params.tag).toBe('ai');
156
+ expect(body.params.skillTag).toBe('nlp');
157
+ expect(body.params.sort).toBe('popular');
158
+ expect(body.params.offset).toBe(10);
159
+ });
160
+
161
+ it('should combine query with new params', async () => {
162
+ setupMockRpcResponse(mockDiscoverResponse);
163
+
164
+ await manager.discoverAgents({
165
+ query: 'weather',
166
+ tag: 'api',
167
+ contentMode: 'application/json',
168
+ sort: 'recent',
169
+ limit: 25,
170
+ });
171
+
172
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
173
+ expect(body.params.query).toBe('weather');
174
+ expect(body.params.tag).toBe('api');
175
+ expect(body.params.contentMode).toBe('application/json');
176
+ expect(body.params.sort).toBe('recent');
177
+ expect(body.params.limit).toBe(25);
178
+ });
179
+ });
180
+
181
+ describe('discoverAgents() edge cases', () => {
182
+ it('should handle empty results', async () => {
183
+ setupMockRpcResponse({ agents: [] });
184
+
185
+ const result = await manager.discoverAgents({ tag: 'nonexistent' });
186
+
187
+ expect(result).toHaveLength(0);
188
+ });
189
+
190
+ it('should handle RPC errors gracefully', async () => {
191
+ mockFetch.mockResolvedValueOnce({
192
+ ok: true,
193
+ json: () => Promise.resolve({ error: { message: 'Not found' } }),
194
+ });
195
+
196
+ const result = await manager.discoverAgents({ tag: 'test' });
197
+
198
+ expect(result).toEqual([]);
199
+ });
200
+
201
+ it('should handle network errors', async () => {
202
+ mockFetch.mockRejectedValueOnce(new Error('Network error'));
203
+
204
+ const result = await manager.discoverAgents({ tag: 'test' });
205
+
206
+ expect(result).toEqual([]);
207
+ });
208
+
209
+ it('should return empty array when apiKey not configured', async () => {
210
+ const managerNoKey = new A2AConnectionManager({
211
+ enabled: true,
212
+ // No apiKey
213
+ });
214
+
215
+ const result = await managerNoKey.discoverAgents({ tag: 'test' });
216
+
217
+ expect(result).toEqual([]);
218
+ expect(mockFetch).not.toHaveBeenCalled();
219
+ });
220
+ });
221
+
222
+ describe('discoverAgents() sort values', () => {
223
+ it('should accept rating sort', async () => {
224
+ setupMockRpcResponse(mockDiscoverResponse);
225
+ await manager.discoverAgents({ sort: 'rating' });
226
+
227
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
228
+ expect(body.params.sort).toBe('rating');
229
+ });
230
+
231
+ it('should accept popular sort', async () => {
232
+ setupMockRpcResponse(mockDiscoverResponse);
233
+ await manager.discoverAgents({ sort: 'popular' });
234
+
235
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
236
+ expect(body.params.sort).toBe('popular');
237
+ });
238
+
239
+ it('should accept recent sort', async () => {
240
+ setupMockRpcResponse(mockDiscoverResponse);
241
+ await manager.discoverAgents({ sort: 'recent' });
242
+
243
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
244
+ expect(body.params.sort).toBe('recent');
245
+ });
246
+ });
247
+
248
+ describe('discoverAgents() response mapping', () => {
249
+ it('should map response correctly', async () => {
250
+ setupMockRpcResponse({
251
+ agents: [
252
+ {
253
+ id: 'agent-1',
254
+ name: 'Test Agent',
255
+ description: 'A test agent',
256
+ verified: true,
257
+ tenantName: 'TestTenant',
258
+ avgRating: 4.5,
259
+ },
260
+ ],
261
+ });
262
+
263
+ const result = await manager.discoverAgents({ query: 'test' });
264
+
265
+ expect(result).toEqual([
266
+ {
267
+ id: 'agent-1',
268
+ name: 'Test Agent',
269
+ description: 'A test agent',
270
+ verified: true,
271
+ tenantName: 'TestTenant',
272
+ avgRating: 4.5,
273
+ },
274
+ ]);
275
+ });
276
+ });
277
+
278
+ describe('listAvailableAgents()', () => {
279
+ it('should list available agents', async () => {
280
+ setupMockRpcResponse({
281
+ agents: [
282
+ {
283
+ id: 'agent-1',
284
+ name: 'Test Agent',
285
+ description: 'A test agent',
286
+ verified: true,
287
+ accessType: 'same-tenant',
288
+ },
289
+ ],
290
+ });
291
+
292
+ const result = await manager.listAvailableAgents();
293
+
294
+ expect(result).toHaveLength(1);
295
+ expect(result[0].accessType).toBe('same-tenant');
296
+ });
297
+ });
298
+ });
package/src/connection.ts CHANGED
@@ -427,7 +427,8 @@ export class A2AConnectionManager {
427
427
  skillTag?: string;
428
428
  contentMode?: string;
429
429
  sort?: string;
430
- verified?: boolean;
430
+ owner?: string; // Filter by organization/tenant name
431
+ verified?: boolean; // Only show agents from verified organizations
431
432
  limit?: number;
432
433
  offset?: number;
433
434
  scope?: string;
@@ -451,6 +452,51 @@ export class A2AConnectionManager {
451
452
  return result?.agents || [];
452
453
  }
453
454
 
455
+ /**
456
+ * Discover agents near a geographic location
457
+ */
458
+ async discoverNearby(options: {
459
+ lat: number;
460
+ lng: number;
461
+ radius?: number;
462
+ tag?: string;
463
+ category?: string;
464
+ limit?: number;
465
+ offset?: number;
466
+ }): Promise<Array<{
467
+ id: string;
468
+ name: string;
469
+ description?: string;
470
+ verified?: boolean;
471
+ tenantName?: string;
472
+ avgRating?: number;
473
+ location?: {
474
+ name: string;
475
+ lat: number;
476
+ lng: number;
477
+ country: string;
478
+ };
479
+ distance?: number;
480
+ }>> {
481
+ const result = await this.a2aRpc<{ agents: Array<{
482
+ id: string;
483
+ name: string;
484
+ description?: string;
485
+ verified?: boolean;
486
+ tenantName?: string;
487
+ avgRating?: number;
488
+ location?: {
489
+ name: string;
490
+ lat: number;
491
+ lng: number;
492
+ country: string;
493
+ };
494
+ distance?: number;
495
+ }> }>('x-gopherhole/agents.discover.nearby', options);
496
+
497
+ return result?.agents || [];
498
+ }
499
+
454
500
  /**
455
501
  * List connection status (for backward compatibility)
456
502
  */
package/tsconfig.json CHANGED
@@ -13,5 +13,5 @@
13
13
  "forceConsistentCasingInFileNames": true
14
14
  },
15
15
  "include": ["index.ts", "src/**/*.ts"],
16
- "exclude": ["node_modules", "dist"]
16
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
17
17
  }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['src/**/*.test.ts'],
8
+ },
9
+ });