gopherhole_openclaw_a2a 0.3.14 → 0.4.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.
@@ -1,328 +0,0 @@
1
- /**
2
- * Gateway WebSocket Client
3
- * Sends messages to the Clawdbot gateway via its WebSocket protocol
4
- *
5
- * chat.send is non-blocking - it returns a runId immediately and
6
- * streams the response via chat events. We accumulate deltas and
7
- * resolve when we get state: "final".
8
- */
9
-
10
- import WebSocket from 'ws';
11
- import { v4 as uuidv4 } from 'uuid';
12
- import { readFileSync } from 'fs';
13
- import { join } from 'path';
14
- import { homedir } from 'os';
15
-
16
- interface PendingRequest {
17
- resolve: (result: unknown) => void;
18
- reject: (error: Error) => void;
19
- timeout: NodeJS.Timeout;
20
- }
21
-
22
- interface PendingChat {
23
- resolve: (result: { text: string }) => void;
24
- reject: (error: Error) => void;
25
- timeout: NodeJS.Timeout;
26
- latestText: string; // Each delta is the full message so far, not incremental
27
- }
28
-
29
- let ws: WebSocket | null = null;
30
- let pendingRequests: Map<string, PendingRequest> = new Map();
31
- let pendingChats: Map<string, PendingChat> = new Map(); // keyed by runId
32
- let connected = false;
33
- let handshakeComplete = false;
34
-
35
- function getGatewayToken(): string | null {
36
- try {
37
- const configPath = join(homedir(), '.clawdbot', 'clawdbot.json');
38
- const config = JSON.parse(readFileSync(configPath, 'utf8'));
39
- return config?.gateway?.auth?.token ?? null;
40
- } catch {
41
- return null;
42
- }
43
- }
44
-
45
- export async function connectToGateway(port = 18789): Promise<void> {
46
- if (ws && connected && handshakeComplete) return;
47
-
48
- const token = getGatewayToken();
49
-
50
- return new Promise((resolve, reject) => {
51
- ws = new WebSocket(`ws://127.0.0.1:${port}`);
52
-
53
- const timeout = setTimeout(() => {
54
- ws?.terminate();
55
- reject(new Error('Gateway connection timeout'));
56
- }, 10000);
57
-
58
- ws.on('open', () => {
59
- console.log('[a2a] Connected to gateway WebSocket, sending handshake...');
60
- connected = true;
61
-
62
- // Send connect handshake
63
- const connectId = uuidv4();
64
-
65
- const connectFrame = {
66
- type: 'req',
67
- id: connectId,
68
- method: 'connect',
69
- params: {
70
- minProtocol: 3,
71
- maxProtocol: 3,
72
- client: {
73
- id: 'gateway-client',
74
- displayName: 'A2A Channel Plugin',
75
- version: '0.3.4',
76
- platform: process.platform,
77
- mode: 'backend',
78
- },
79
- caps: [],
80
- auth: token ? { token } : undefined,
81
- role: 'operator',
82
- scopes: ['operator.admin'],
83
- },
84
- };
85
-
86
- pendingRequests.set(connectId, {
87
- resolve: () => {
88
- clearTimeout(timeout);
89
- handshakeComplete = true;
90
- console.log('[a2a] Gateway handshake complete');
91
- resolve();
92
- },
93
- reject: (err) => {
94
- clearTimeout(timeout);
95
- reject(err);
96
- },
97
- timeout: setTimeout(() => {
98
- pendingRequests.delete(connectId);
99
- reject(new Error('Gateway handshake timeout'));
100
- }, 5000),
101
- });
102
-
103
- ws!.send(JSON.stringify(connectFrame));
104
- });
105
-
106
- ws.on('message', (data) => {
107
- try {
108
- const msg = JSON.parse(data.toString());
109
-
110
- // Log all non-tick messages for debugging
111
- if (msg.type !== 'tick') {
112
- console.log(`[a2a] Gateway msg: type=${msg.type}, event=${msg.event ?? 'n/a'}, id=${msg.id ?? 'n/a'}`);
113
- }
114
-
115
- // Handle response frames (for RPC calls)
116
- if (msg.type === 'res' && msg.id && pendingRequests.has(msg.id)) {
117
- const pending = pendingRequests.get(msg.id)!;
118
- clearTimeout(pending.timeout);
119
- pendingRequests.delete(msg.id);
120
-
121
- if (msg.error) {
122
- pending.reject(new Error(msg.error.message || 'RPC error'));
123
- } else {
124
- // Protocol uses 'payload' not 'result'
125
- pending.resolve(msg.payload);
126
- }
127
- return;
128
- }
129
-
130
- // Handle chat events (streaming responses)
131
- if (msg.type === 'event' && msg.event === 'chat') {
132
- handleChatEvent(msg.payload);
133
- return;
134
- }
135
-
136
- // Handle tick frames (keepalive)
137
- if (msg.type === 'tick') {
138
- // Could respond with tick ack if needed
139
- }
140
- } catch (err) {
141
- console.error('[a2a] Failed to parse gateway message:', err);
142
- }
143
- });
144
-
145
- ws.on('close', () => {
146
- connected = false;
147
- handshakeComplete = false;
148
- ws = null;
149
- console.log('[a2a] Disconnected from gateway WebSocket');
150
-
151
- // Reject all pending chats
152
- for (const [runId, pending] of pendingChats) {
153
- clearTimeout(pending.timeout);
154
- pending.reject(new Error('Gateway connection closed'));
155
- }
156
- pendingChats.clear();
157
- });
158
-
159
- ws.on('error', (err) => {
160
- clearTimeout(timeout);
161
- console.error('[a2a] Gateway WebSocket error:', err.message);
162
- reject(err);
163
- });
164
- });
165
- }
166
-
167
- function extractTextContent(content: unknown): string {
168
- if (typeof content === 'string') {
169
- return content;
170
- }
171
- if (Array.isArray(content)) {
172
- // Content blocks array: [{ type: 'text', text: '...' }, ...]
173
- return content
174
- .filter((block: unknown) =>
175
- typeof block === 'object' && block !== null &&
176
- (block as Record<string, unknown>).type === 'text'
177
- )
178
- .map((block: unknown) => (block as { text?: string }).text ?? '')
179
- .join('');
180
- }
181
- if (typeof content === 'object' && content !== null) {
182
- // Single content block or unknown structure
183
- const obj = content as Record<string, unknown>;
184
- if (obj.text && typeof obj.text === 'string') {
185
- return obj.text;
186
- }
187
- }
188
- return '';
189
- }
190
-
191
- function handleChatEvent(payload: {
192
- runId: string;
193
- sessionKey: string;
194
- seq: number;
195
- state: 'delta' | 'final' | 'aborted' | 'error';
196
- message?: { role?: string; content?: unknown };
197
- errorMessage?: string;
198
- }): void {
199
- const pending = pendingChats.get(payload.runId);
200
- if (!pending) {
201
- // Not a chat we're tracking
202
- return;
203
- }
204
-
205
- // Detailed logging for debugging relay issues
206
- console.log(`[a2a] Chat event: runId=${payload.runId}, state=${payload.state}, seq=${payload.seq}, role=${payload.message?.role}`);
207
-
208
- if (payload.state === 'delta' || payload.state === 'final') {
209
- console.log(`[a2a] Chat content (${payload.state}): ${JSON.stringify(payload.message?.content)?.slice(0, 300)}`);
210
- }
211
-
212
- if (payload.state === 'delta') {
213
- // Each delta contains the full message so far, not incremental
214
- if (payload.message?.role === 'assistant' && payload.message?.content) {
215
- const text = extractTextContent(payload.message.content);
216
- if (text) {
217
- pending.latestText = text;
218
- console.log(`[a2a] Updated latestText (delta): "${text.slice(0, 100)}..." (len=${text.length})`);
219
- }
220
- } else {
221
- console.log(`[a2a] Skipping delta - role=${payload.message?.role}, hasContent=${!!payload.message?.content}`);
222
- }
223
- } else if (payload.state === 'final') {
224
- // Final message - resolve with latest text
225
- clearTimeout(pending.timeout);
226
- pendingChats.delete(payload.runId);
227
-
228
- // Use final content if available, otherwise use latest delta
229
- if (payload.message?.role === 'assistant' && payload.message?.content) {
230
- const text = extractTextContent(payload.message.content);
231
- if (text) {
232
- pending.latestText = text;
233
- console.log(`[a2a] Updated latestText (final): "${text.slice(0, 100)}..." (len=${text.length})`);
234
- }
235
- }
236
-
237
- console.log(`[a2a] Chat complete - resolving with: "${pending.latestText.slice(0, 150)}..." (total ${pending.latestText.length} chars)`);
238
- pending.resolve({ text: pending.latestText });
239
- } else if (payload.state === 'error' || payload.state === 'aborted') {
240
- clearTimeout(pending.timeout);
241
- pendingChats.delete(payload.runId);
242
- console.error(`[a2a] Chat ${payload.state}: ${payload.errorMessage}`);
243
- pending.reject(new Error(payload.errorMessage || `Chat ${payload.state}`));
244
- }
245
- }
246
-
247
- export async function callGateway(method: string, params: Record<string, unknown>, timeoutMs = 300000): Promise<unknown> {
248
- if (!ws || !connected || !handshakeComplete) {
249
- await connectToGateway();
250
- }
251
-
252
- const id = uuidv4();
253
-
254
- return new Promise((resolve, reject) => {
255
- const timeout = setTimeout(() => {
256
- pendingRequests.delete(id);
257
- reject(new Error('Gateway request timeout'));
258
- }, timeoutMs);
259
-
260
- pendingRequests.set(id, { resolve, reject, timeout });
261
-
262
- const frame = {
263
- type: 'req',
264
- id,
265
- method,
266
- params,
267
- };
268
-
269
- ws!.send(JSON.stringify(frame));
270
- });
271
- }
272
-
273
- /**
274
- * Send a chat message and wait for the full response.
275
- * chat.send is non-blocking - returns runId immediately, response streams via events.
276
- */
277
- export async function sendChatMessage(
278
- sessionKey: string,
279
- message: string,
280
- timeoutMs = 300000
281
- ): Promise<{ text: string }> {
282
- if (!ws || !connected || !handshakeComplete) {
283
- await connectToGateway();
284
- }
285
-
286
- const idempotencyKey = uuidv4();
287
-
288
- // First, send the chat.send request
289
- console.log(`[a2a] Sending chat.send: sessionKey=${sessionKey}, idempotencyKey=${idempotencyKey}`);
290
-
291
- const result = await callGateway('chat.send', {
292
- sessionKey,
293
- message,
294
- idempotencyKey,
295
- }) as { runId: string; status: string } | undefined;
296
-
297
- console.log(`[a2a] chat.send raw result:`, JSON.stringify(result));
298
-
299
- if (!result.runId) {
300
- throw new Error('chat.send did not return a runId');
301
- }
302
-
303
- // Now wait for chat events to complete
304
- return new Promise((resolve, reject) => {
305
- const timeout = setTimeout(() => {
306
- pendingChats.delete(result.runId);
307
- reject(new Error('Chat response timeout'));
308
- }, timeoutMs);
309
-
310
- pendingChats.set(result.runId, {
311
- resolve,
312
- reject,
313
- timeout,
314
- latestText: '',
315
- });
316
- });
317
- }
318
-
319
- export function disconnectFromGateway(): void {
320
- if (ws) {
321
- ws.close();
322
- ws = null;
323
- connected = false;
324
- handshakeComplete = false;
325
- }
326
- pendingChats.clear();
327
- pendingRequests.clear();
328
- }
package/src/logger.ts DELETED
@@ -1,118 +0,0 @@
1
- /**
2
- * A2A Plugin Logger
3
- * Writes to both console and a dedicated log file for dashboard viewing
4
- */
5
-
6
- import { appendFileSync, mkdirSync, existsSync } from 'fs';
7
- import { join } from 'path';
8
- import { homedir } from 'os';
9
-
10
- const LOG_DIR = join(homedir(), '.clawdbot', 'logs');
11
- const A2A_LOG_FILE = join(LOG_DIR, 'a2a.log');
12
-
13
- // Ensure log directory exists
14
- try {
15
- if (!existsSync(LOG_DIR)) {
16
- mkdirSync(LOG_DIR, { recursive: true });
17
- }
18
- } catch {
19
- // Ignore if we can't create the directory
20
- }
21
-
22
- export interface A2ALogEntry {
23
- timestamp: string;
24
- level: 'info' | 'warn' | 'error' | 'debug';
25
- event: string;
26
- taskId?: string;
27
- from?: string;
28
- to?: string;
29
- message?: string;
30
- data?: Record<string, unknown>;
31
- }
32
-
33
- function writeLog(entry: A2ALogEntry): void {
34
- const line = JSON.stringify(entry) + '\n';
35
-
36
- // Console output (colored)
37
- const prefix = `[a2a:${entry.event}]`;
38
- const msg = entry.message || '';
39
-
40
- switch (entry.level) {
41
- case 'error':
42
- console.error(prefix, msg, entry.data || '');
43
- break;
44
- case 'warn':
45
- console.warn(prefix, msg, entry.data || '');
46
- break;
47
- default:
48
- console.log(prefix, msg, entry.data || '');
49
- }
50
-
51
- // File output (JSON lines)
52
- try {
53
- appendFileSync(A2A_LOG_FILE, line);
54
- } catch {
55
- // Ignore file write errors
56
- }
57
- }
58
-
59
- export const a2aLog = {
60
- info(event: string, message: string, data?: Record<string, unknown>): void {
61
- writeLog({ timestamp: new Date().toISOString(), level: 'info', event, message, data });
62
- },
63
-
64
- warn(event: string, message: string, data?: Record<string, unknown>): void {
65
- writeLog({ timestamp: new Date().toISOString(), level: 'warn', event, message, data });
66
- },
67
-
68
- error(event: string, message: string, data?: Record<string, unknown>): void {
69
- writeLog({ timestamp: new Date().toISOString(), level: 'error', event, message, data });
70
- },
71
-
72
- // Structured event logging for dashboard
73
- messageReceived(from: string, taskId: string | undefined, text: string): void {
74
- writeLog({
75
- timestamp: new Date().toISOString(),
76
- level: 'info',
77
- event: 'message_received',
78
- from,
79
- taskId,
80
- message: text.slice(0, 200),
81
- data: { textLength: text.length },
82
- });
83
- },
84
-
85
- messageProcessing(taskId: string | undefined, sessionKey: string): void {
86
- writeLog({
87
- timestamp: new Date().toISOString(),
88
- level: 'info',
89
- event: 'message_processing',
90
- taskId,
91
- data: { sessionKey },
92
- });
93
- },
94
-
95
- responseCaptured(taskId: string | undefined, text: string): void {
96
- writeLog({
97
- timestamp: new Date().toISOString(),
98
- level: 'info',
99
- event: 'response_captured',
100
- taskId,
101
- message: text.slice(0, 200),
102
- data: { textLength: text.length },
103
- });
104
- },
105
-
106
- responseSent(taskId: string, to: string, success: boolean): void {
107
- writeLog({
108
- timestamp: new Date().toISOString(),
109
- level: success ? 'info' : 'error',
110
- event: 'response_sent',
111
- taskId,
112
- to,
113
- data: { success },
114
- });
115
- },
116
- };
117
-
118
- export const A2A_LOG_FILE_PATH = A2A_LOG_FILE;
package/src/types.ts DELETED
@@ -1,75 +0,0 @@
1
- /**
2
- * A2A Protocol Types
3
- * Compatible with @gopherhole/sdk
4
- */
5
-
6
- export interface A2AMessage {
7
- type: 'message' | 'response' | 'chunk' | 'status';
8
- taskId: string;
9
- contextId?: string;
10
- from?: string;
11
- content?: {
12
- parts: Array<{ kind: string; text?: string; data?: unknown; mimeType?: string }>;
13
- };
14
- status?: 'working' | 'completed' | 'failed' | 'canceled';
15
- error?: string;
16
- }
17
-
18
- export interface A2ASkill {
19
- id: string;
20
- name: string;
21
- description?: string;
22
- tags?: string[];
23
- examples?: string[];
24
- inputModes?: string[];
25
- outputModes?: string[];
26
- }
27
-
28
- export interface A2AAgentCard {
29
- name: string;
30
- description?: string;
31
- url?: string;
32
- version?: string;
33
- skills?: A2ASkill[];
34
- }
35
-
36
- export interface A2AResponse {
37
- text: string;
38
- status: string;
39
- from?: string;
40
- }
41
-
42
- /**
43
- * A2A Channel Config (flat structure)
44
- *
45
- * Example:
46
- * {
47
- * "channels": {
48
- * "a2a": {
49
- * "enabled": true,
50
- * "bridgeUrl": "wss://hub.gopherhole.ai/ws",
51
- * "apiKey": "gph_your_api_key"
52
- * }
53
- * }
54
- * }
55
- */
56
- export interface A2AChannelConfig {
57
- enabled?: boolean;
58
- bridgeUrl?: string; // WebSocket URL (default: wss://hub.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)
65
- }
66
-
67
- export interface ResolvedA2AAccount {
68
- accountId: string;
69
- name: string;
70
- enabled: boolean;
71
- configured: boolean;
72
- agentId: string;
73
- bridgeUrl: string | null;
74
- config: A2AChannelConfig;
75
- }
package/test-image.mjs DELETED
@@ -1,29 +0,0 @@
1
- import { GopherHole } from '@gopherhole/sdk';
2
- import { readFileSync } from 'fs';
3
-
4
- const gph = new GopherHole({
5
- apiKey: 'gph_a3ed7c3f30e5415e9dc92b72c1c05b78',
6
- hubUrl: 'wss://gopherhole.ai/ws',
7
- });
8
-
9
- await gph.connect();
10
- console.log('Connected, sending image...');
11
-
12
- const imagePath = '/Users/brettwaterson/.marketclaw/images/1771822365256-A3kAAzoE.jpg';
13
- const imageData = readFileSync(imagePath).toString('base64');
14
- console.log('Image size:', imageData.length, 'chars');
15
-
16
- const task = await gph.send('agent-70153299', {
17
- role: 'agent',
18
- parts: [
19
- { kind: 'text', text: 'What do you see in this image? Please describe it.' },
20
- { kind: 'data', mimeType: 'image/jpeg', data: imageData },
21
- ],
22
- });
23
-
24
- console.log('Task created:', task.id);
25
-
26
- const completed = await gph.waitForTask(task.id, { maxWaitMs: 60000 });
27
- console.log('Response:', completed.artifacts?.[0]?.parts?.[0]?.text || JSON.stringify(completed));
28
-
29
- gph.disconnect();
package/test-image2.mjs DELETED
@@ -1,37 +0,0 @@
1
- import { GopherHole } from '@gopherhole/sdk';
2
- import { readFileSync } from 'fs';
3
-
4
- const gph = new GopherHole({
5
- apiKey: 'gph_a3ed7c3f30e5415e9dc92b72c1c05b78',
6
- hubUrl: 'wss://gopherhole.ai/ws',
7
- });
8
-
9
- await gph.connect();
10
- console.log('Connected as:', gph.id);
11
-
12
- const imagePath = '/Users/brettwaterson/.marketclaw/images/1771822365256-A3kAAzoE.jpg';
13
- const imageData = readFileSync(imagePath).toString('base64');
14
- console.log('Image size:', imageData.length, 'chars (base64)');
15
-
16
- const payload = {
17
- role: 'agent',
18
- parts: [
19
- { kind: 'text', text: 'Describe this image please.' },
20
- { kind: 'data', mimeType: 'image/jpeg', data: imageData },
21
- ],
22
- };
23
-
24
- console.log('Sending payload with', payload.parts.length, 'parts');
25
- console.log('Part 0:', { kind: payload.parts[0].kind, hasText: !!payload.parts[0].text });
26
- console.log('Part 1:', { kind: payload.parts[1].kind, mimeType: payload.parts[1].mimeType, dataLen: payload.parts[1].data?.length });
27
-
28
- const task = await gph.send('agent-70153299', payload);
29
- console.log('Task:', task.id, 'Status:', task.status?.state);
30
-
31
- const completed = await gph.waitForTask(task.id, { maxWaitMs: 60000 });
32
- console.log('Final status:', completed.status?.state);
33
-
34
- const response = completed.artifacts?.[0]?.parts?.[0]?.text;
35
- console.log('Response:', response?.slice(0, 300) || 'No text in response');
36
-
37
- gph.disconnect();
package/tsconfig.json DELETED
@@ -1,17 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "declaration": true,
7
- "outDir": "dist",
8
- "rootDir": ".",
9
- "strict": false,
10
- "noImplicitAny": false,
11
- "esModuleInterop": true,
12
- "skipLibCheck": true,
13
- "forceConsistentCasingInFileNames": true
14
- },
15
- "include": ["index.ts", "src/**/*.ts"],
16
- "exclude": ["node_modules", "dist", "**/*.test.ts"]
17
- }
package/vitest.config.ts DELETED
@@ -1,9 +0,0 @@
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
- });
File without changes