gopherhole_openclaw_a2a 0.1.2
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 +67 -0
- package/SKILL.md +84 -0
- package/clawdbot.plugin.json +9 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +82 -0
- package/dist/src/channel.d.ts +68 -0
- package/dist/src/channel.js +271 -0
- package/dist/src/connection.d.ts +62 -0
- package/dist/src/connection.js +513 -0
- package/dist/src/gateway-client.d.ts +18 -0
- package/dist/src/gateway-client.js +256 -0
- package/dist/src/types.d.ts +73 -0
- package/dist/src/types.js +5 -0
- package/index.ts +100 -0
- package/package.json +46 -0
- package/src/channel.ts +377 -0
- package/src/connection.ts +605 -0
- package/src/gateway-client.ts +317 -0
- package/src/types.ts +73 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,256 @@
|
|
|
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
|
+
import WebSocket from 'ws';
|
|
10
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
11
|
+
import { readFileSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { homedir } from 'os';
|
|
14
|
+
let ws = null;
|
|
15
|
+
let pendingRequests = new Map();
|
|
16
|
+
let pendingChats = new Map(); // keyed by runId
|
|
17
|
+
let connected = false;
|
|
18
|
+
let handshakeComplete = false;
|
|
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;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function connectToGateway(port = 18789) {
|
|
30
|
+
if (ws && connected && handshakeComplete)
|
|
31
|
+
return;
|
|
32
|
+
const token = getGatewayToken();
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
ws = new WebSocket(`ws://127.0.0.1:${port}`);
|
|
35
|
+
const timeout = setTimeout(() => {
|
|
36
|
+
ws?.terminate();
|
|
37
|
+
reject(new Error('Gateway connection timeout'));
|
|
38
|
+
}, 10000);
|
|
39
|
+
ws.on('open', () => {
|
|
40
|
+
console.log('[a2a] Connected to gateway WebSocket, sending handshake...');
|
|
41
|
+
connected = true;
|
|
42
|
+
// Send connect handshake
|
|
43
|
+
const connectId = uuidv4();
|
|
44
|
+
const connectFrame = {
|
|
45
|
+
type: 'req',
|
|
46
|
+
id: connectId,
|
|
47
|
+
method: 'connect',
|
|
48
|
+
params: {
|
|
49
|
+
minProtocol: 3,
|
|
50
|
+
maxProtocol: 3,
|
|
51
|
+
client: {
|
|
52
|
+
id: 'gateway-client',
|
|
53
|
+
displayName: 'A2A Channel Plugin',
|
|
54
|
+
version: '0.1.0',
|
|
55
|
+
platform: process.platform,
|
|
56
|
+
mode: 'backend',
|
|
57
|
+
},
|
|
58
|
+
caps: [],
|
|
59
|
+
auth: token ? { token } : undefined,
|
|
60
|
+
role: 'operator',
|
|
61
|
+
scopes: ['operator.admin'],
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
pendingRequests.set(connectId, {
|
|
65
|
+
resolve: () => {
|
|
66
|
+
clearTimeout(timeout);
|
|
67
|
+
handshakeComplete = true;
|
|
68
|
+
console.log('[a2a] Gateway handshake complete');
|
|
69
|
+
resolve();
|
|
70
|
+
},
|
|
71
|
+
reject: (err) => {
|
|
72
|
+
clearTimeout(timeout);
|
|
73
|
+
reject(err);
|
|
74
|
+
},
|
|
75
|
+
timeout: setTimeout(() => {
|
|
76
|
+
pendingRequests.delete(connectId);
|
|
77
|
+
reject(new Error('Gateway handshake timeout'));
|
|
78
|
+
}, 5000),
|
|
79
|
+
});
|
|
80
|
+
ws.send(JSON.stringify(connectFrame));
|
|
81
|
+
});
|
|
82
|
+
ws.on('message', (data) => {
|
|
83
|
+
try {
|
|
84
|
+
const msg = JSON.parse(data.toString());
|
|
85
|
+
// Log all non-tick messages for debugging
|
|
86
|
+
if (msg.type !== 'tick') {
|
|
87
|
+
console.log(`[a2a] Gateway msg: type=${msg.type}, event=${msg.event ?? 'n/a'}, id=${msg.id ?? 'n/a'}`);
|
|
88
|
+
}
|
|
89
|
+
// Handle response frames (for RPC calls)
|
|
90
|
+
if (msg.type === 'res' && msg.id && pendingRequests.has(msg.id)) {
|
|
91
|
+
const pending = pendingRequests.get(msg.id);
|
|
92
|
+
clearTimeout(pending.timeout);
|
|
93
|
+
pendingRequests.delete(msg.id);
|
|
94
|
+
if (msg.error) {
|
|
95
|
+
pending.reject(new Error(msg.error.message || 'RPC error'));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// Protocol uses 'payload' not 'result'
|
|
99
|
+
pending.resolve(msg.payload);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Handle chat events (streaming responses)
|
|
104
|
+
if (msg.type === 'event' && msg.event === 'chat') {
|
|
105
|
+
handleChatEvent(msg.payload);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Handle tick frames (keepalive)
|
|
109
|
+
if (msg.type === 'tick') {
|
|
110
|
+
// Could respond with tick ack if needed
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
console.error('[a2a] Failed to parse gateway message:', err);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
ws.on('close', () => {
|
|
118
|
+
connected = false;
|
|
119
|
+
handshakeComplete = false;
|
|
120
|
+
ws = null;
|
|
121
|
+
console.log('[a2a] Disconnected from gateway WebSocket');
|
|
122
|
+
// Reject all pending chats
|
|
123
|
+
for (const [runId, pending] of pendingChats) {
|
|
124
|
+
clearTimeout(pending.timeout);
|
|
125
|
+
pending.reject(new Error('Gateway connection closed'));
|
|
126
|
+
}
|
|
127
|
+
pendingChats.clear();
|
|
128
|
+
});
|
|
129
|
+
ws.on('error', (err) => {
|
|
130
|
+
clearTimeout(timeout);
|
|
131
|
+
console.error('[a2a] Gateway WebSocket error:', err.message);
|
|
132
|
+
reject(err);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function extractTextContent(content) {
|
|
137
|
+
if (typeof content === 'string') {
|
|
138
|
+
return content;
|
|
139
|
+
}
|
|
140
|
+
if (Array.isArray(content)) {
|
|
141
|
+
// Content blocks array: [{ type: 'text', text: '...' }, ...]
|
|
142
|
+
return content
|
|
143
|
+
.filter((block) => typeof block === 'object' && block !== null &&
|
|
144
|
+
block.type === 'text')
|
|
145
|
+
.map((block) => block.text ?? '')
|
|
146
|
+
.join('');
|
|
147
|
+
}
|
|
148
|
+
if (typeof content === 'object' && content !== null) {
|
|
149
|
+
// Single content block or unknown structure
|
|
150
|
+
const obj = content;
|
|
151
|
+
if (obj.text && typeof obj.text === 'string') {
|
|
152
|
+
return obj.text;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return '';
|
|
156
|
+
}
|
|
157
|
+
function handleChatEvent(payload) {
|
|
158
|
+
const pending = pendingChats.get(payload.runId);
|
|
159
|
+
if (!pending) {
|
|
160
|
+
// Not a chat we're tracking
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
console.log(`[a2a] Chat event: runId=${payload.runId}, state=${payload.state}, seq=${payload.seq}, content=${JSON.stringify(payload.message?.content)?.slice(0, 200)}`);
|
|
164
|
+
if (payload.state === 'delta') {
|
|
165
|
+
// Each delta contains the full message so far, not incremental
|
|
166
|
+
if (payload.message?.role === 'assistant' && payload.message?.content) {
|
|
167
|
+
const text = extractTextContent(payload.message.content);
|
|
168
|
+
if (text) {
|
|
169
|
+
pending.latestText = text;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else if (payload.state === 'final') {
|
|
174
|
+
// Final message - resolve with latest text
|
|
175
|
+
clearTimeout(pending.timeout);
|
|
176
|
+
pendingChats.delete(payload.runId);
|
|
177
|
+
// Use final content if available, otherwise use latest delta
|
|
178
|
+
if (payload.message?.role === 'assistant' && payload.message?.content) {
|
|
179
|
+
const text = extractTextContent(payload.message.content);
|
|
180
|
+
if (text) {
|
|
181
|
+
pending.latestText = text;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
console.log(`[a2a] Chat complete: ${pending.latestText.slice(0, 100)}...`);
|
|
185
|
+
pending.resolve({ text: pending.latestText });
|
|
186
|
+
}
|
|
187
|
+
else if (payload.state === 'error' || payload.state === 'aborted') {
|
|
188
|
+
clearTimeout(pending.timeout);
|
|
189
|
+
pendingChats.delete(payload.runId);
|
|
190
|
+
pending.reject(new Error(payload.errorMessage || `Chat ${payload.state}`));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
export async function callGateway(method, params, timeoutMs = 300000) {
|
|
194
|
+
if (!ws || !connected || !handshakeComplete) {
|
|
195
|
+
await connectToGateway();
|
|
196
|
+
}
|
|
197
|
+
const id = uuidv4();
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
const timeout = setTimeout(() => {
|
|
200
|
+
pendingRequests.delete(id);
|
|
201
|
+
reject(new Error('Gateway request timeout'));
|
|
202
|
+
}, timeoutMs);
|
|
203
|
+
pendingRequests.set(id, { resolve, reject, timeout });
|
|
204
|
+
const frame = {
|
|
205
|
+
type: 'req',
|
|
206
|
+
id,
|
|
207
|
+
method,
|
|
208
|
+
params,
|
|
209
|
+
};
|
|
210
|
+
ws.send(JSON.stringify(frame));
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Send a chat message and wait for the full response.
|
|
215
|
+
* chat.send is non-blocking - returns runId immediately, response streams via events.
|
|
216
|
+
*/
|
|
217
|
+
export async function sendChatMessage(sessionKey, message, timeoutMs = 300000) {
|
|
218
|
+
if (!ws || !connected || !handshakeComplete) {
|
|
219
|
+
await connectToGateway();
|
|
220
|
+
}
|
|
221
|
+
const idempotencyKey = uuidv4();
|
|
222
|
+
// First, send the chat.send request
|
|
223
|
+
console.log(`[a2a] Sending chat.send: sessionKey=${sessionKey}, idempotencyKey=${idempotencyKey}`);
|
|
224
|
+
const result = await callGateway('chat.send', {
|
|
225
|
+
sessionKey,
|
|
226
|
+
message,
|
|
227
|
+
idempotencyKey,
|
|
228
|
+
});
|
|
229
|
+
console.log(`[a2a] chat.send raw result:`, JSON.stringify(result));
|
|
230
|
+
if (!result.runId) {
|
|
231
|
+
throw new Error('chat.send did not return a runId');
|
|
232
|
+
}
|
|
233
|
+
// Now wait for chat events to complete
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
const timeout = setTimeout(() => {
|
|
236
|
+
pendingChats.delete(result.runId);
|
|
237
|
+
reject(new Error('Chat response timeout'));
|
|
238
|
+
}, timeoutMs);
|
|
239
|
+
pendingChats.set(result.runId, {
|
|
240
|
+
resolve,
|
|
241
|
+
reject,
|
|
242
|
+
timeout,
|
|
243
|
+
latestText: '',
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
export function disconnectFromGateway() {
|
|
248
|
+
if (ws) {
|
|
249
|
+
ws.close();
|
|
250
|
+
ws = null;
|
|
251
|
+
connected = false;
|
|
252
|
+
handshakeComplete = false;
|
|
253
|
+
}
|
|
254
|
+
pendingChats.clear();
|
|
255
|
+
pendingRequests.clear();
|
|
256
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Protocol Types
|
|
3
|
+
* Compatible with MarketClaw's A2A implementation
|
|
4
|
+
*/
|
|
5
|
+
export interface A2AMessage {
|
|
6
|
+
type: 'message' | 'response' | 'chunk' | 'status';
|
|
7
|
+
taskId: string;
|
|
8
|
+
contextId?: string;
|
|
9
|
+
from?: string;
|
|
10
|
+
content?: {
|
|
11
|
+
parts: Array<{
|
|
12
|
+
kind: string;
|
|
13
|
+
text?: string;
|
|
14
|
+
data?: unknown;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
status?: 'working' | 'completed' | 'failed' | 'canceled';
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface A2AAgentCard {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
skills?: string[];
|
|
25
|
+
url?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface A2APendingRequest {
|
|
28
|
+
taskId: string;
|
|
29
|
+
resolve: (response: A2AResponse) => void;
|
|
30
|
+
reject: (error: Error) => void;
|
|
31
|
+
timeout: NodeJS.Timeout;
|
|
32
|
+
startedAt: number;
|
|
33
|
+
}
|
|
34
|
+
export interface A2AResponse {
|
|
35
|
+
text: string;
|
|
36
|
+
status: string;
|
|
37
|
+
from?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface A2AChannelConfig {
|
|
40
|
+
enabled?: boolean;
|
|
41
|
+
agentId?: string;
|
|
42
|
+
agentName?: string;
|
|
43
|
+
bridgeUrl?: string;
|
|
44
|
+
agents?: Array<{
|
|
45
|
+
id: string;
|
|
46
|
+
url: string;
|
|
47
|
+
name?: string;
|
|
48
|
+
}>;
|
|
49
|
+
gopherhole?: {
|
|
50
|
+
enabled?: boolean;
|
|
51
|
+
apiKey: string;
|
|
52
|
+
hubUrl?: string;
|
|
53
|
+
};
|
|
54
|
+
auth?: {
|
|
55
|
+
token?: string;
|
|
56
|
+
};
|
|
57
|
+
reconnectIntervalMs?: number;
|
|
58
|
+
requestTimeoutMs?: number;
|
|
59
|
+
}
|
|
60
|
+
export interface ResolvedA2AAccount {
|
|
61
|
+
accountId: string;
|
|
62
|
+
name: string;
|
|
63
|
+
enabled: boolean;
|
|
64
|
+
configured: boolean;
|
|
65
|
+
agentId: string;
|
|
66
|
+
bridgeUrl: string | null;
|
|
67
|
+
agents: Array<{
|
|
68
|
+
id: string;
|
|
69
|
+
url: string;
|
|
70
|
+
name?: string;
|
|
71
|
+
}>;
|
|
72
|
+
config: A2AChannelConfig;
|
|
73
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
|
|
8
|
+
// Minimal plugin interface
|
|
9
|
+
interface ClawdbotPluginApi {
|
|
10
|
+
runtime: unknown;
|
|
11
|
+
registerChannel(opts: { plugin: unknown }): void;
|
|
12
|
+
registerTool?(opts: {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
parameters: unknown;
|
|
16
|
+
execute: (id: string, params: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text: string }> }>;
|
|
17
|
+
}): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const plugin = {
|
|
21
|
+
id: 'gopherhole_openclaw_a2a',
|
|
22
|
+
name: 'A2A Protocol',
|
|
23
|
+
description: 'Agent-to-Agent communication channel',
|
|
24
|
+
configSchema: { type: 'object', additionalProperties: false, properties: {} },
|
|
25
|
+
register(api: ClawdbotPluginApi) {
|
|
26
|
+
setA2ARuntime(api.runtime);
|
|
27
|
+
api.registerChannel({ plugin: a2aPlugin });
|
|
28
|
+
|
|
29
|
+
// Register a tool for interacting with connected agents
|
|
30
|
+
api.registerTool?.({
|
|
31
|
+
name: 'a2a_agents',
|
|
32
|
+
description: 'List connected A2A agents and send messages to them',
|
|
33
|
+
parameters: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
action: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
enum: ['list', 'send'],
|
|
39
|
+
description: 'Action to perform',
|
|
40
|
+
},
|
|
41
|
+
agentId: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'Target agent ID (for send action)',
|
|
44
|
+
},
|
|
45
|
+
message: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: 'Message to send (for send action)',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
required: ['action'],
|
|
51
|
+
},
|
|
52
|
+
execute: async (_id, params) => {
|
|
53
|
+
const action = params.action as string;
|
|
54
|
+
const agentId = params.agentId as string | undefined;
|
|
55
|
+
const message = params.message as string | undefined;
|
|
56
|
+
|
|
57
|
+
const manager = getA2AConnectionManager();
|
|
58
|
+
if (!manager) {
|
|
59
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: 'A2A channel not running' }) }] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (action === 'list') {
|
|
63
|
+
const agents = manager.listAgents();
|
|
64
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'ok', agents }) }] };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (action === 'send') {
|
|
68
|
+
if (!agentId || !message) {
|
|
69
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: 'agentId and message required for send action' }) }] };
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
// Use sendViaGopherHole for remote agents (routes through the hub)
|
|
73
|
+
// Use sendMessage for direct connections
|
|
74
|
+
const isGopherHoleConnected = manager.isGopherHoleConnected();
|
|
75
|
+
const isDirectConnection = manager.isConnected(agentId) && agentId !== 'gopherhole';
|
|
76
|
+
|
|
77
|
+
let response;
|
|
78
|
+
if (isDirectConnection) {
|
|
79
|
+
// Direct WebSocket connection to the agent
|
|
80
|
+
response = await manager.sendMessage(agentId, message);
|
|
81
|
+
} else if (isGopherHoleConnected) {
|
|
82
|
+
// Route through GopherHole hub
|
|
83
|
+
response = await manager.sendViaGopherHole(agentId, message);
|
|
84
|
+
} else {
|
|
85
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: `Cannot reach agent ${agentId} - no direct connection or GopherHole` }) }] };
|
|
86
|
+
}
|
|
87
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'ok', agentId, response }) }] };
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: (err as Error).message }) }] };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: `Unknown action: ${action}` }) }] };
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default plugin;
|
|
100
|
+
export { getA2AConnectionManager };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gopherhole_openclaw_a2a",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "GopherHole A2A plugin for OpenClaw - connect your AI agent to the GopherHole network",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"clean": "rm -rf dist",
|
|
11
|
+
"prepublishOnly": "npm run build"
|
|
12
|
+
},
|
|
13
|
+
"clawdbot": {
|
|
14
|
+
"id": "gopherhole_openclaw_a2a",
|
|
15
|
+
"extensions": [
|
|
16
|
+
"./dist/index.js"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"openclaw",
|
|
21
|
+
"clawdbot",
|
|
22
|
+
"a2a",
|
|
23
|
+
"gopherhole",
|
|
24
|
+
"agent",
|
|
25
|
+
"ai"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/helixdata/gopherhole.git",
|
|
30
|
+
"directory": "clawdbot-plugin"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://gopherhole.ai",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"uuid": "^10.0.0",
|
|
36
|
+
"ws": "^8.18.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/uuid": "^10.0.0",
|
|
40
|
+
"@types/ws": "^8.5.10",
|
|
41
|
+
"typescript": "^5.9.3"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"clawdbot": "*"
|
|
45
|
+
}
|
|
46
|
+
}
|