solana-traderclaw 1.0.89 → 1.0.91

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.
@@ -0,0 +1,303 @@
1
+ // src/bitquery-ws.ts
2
+ var RECONNECT_DELAYS = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
3
+ var BitqueryStreamManager = class {
4
+ config;
5
+ ws = null;
6
+ authenticated = false;
7
+ connecting = false;
8
+ reconnectAttempt = 0;
9
+ reconnectTimer = null;
10
+ intentionalClose = false;
11
+ currentAccessToken = "";
12
+ // FIFO queue — server doesn't echo a requestId, so we match by arrival order
13
+ pendingSubscribeQueue = [];
14
+ // keyed by subscriptionId
15
+ pendingUnsubscribes = /* @__PURE__ */ new Map();
16
+ // tracks active subscriptions for auto-resubscribe on reconnect
17
+ activeSubscriptions = /* @__PURE__ */ new Map();
18
+ constructor(config) {
19
+ this.config = config;
20
+ }
21
+ async subscribe(params) {
22
+ await this.ensureConnected();
23
+ return new Promise((resolve, reject) => {
24
+ const timeout = setTimeout(() => {
25
+ const idx = this.pendingSubscribeQueue.findIndex((p) => p.resolve === resolve);
26
+ if (idx !== -1) this.pendingSubscribeQueue.splice(idx, 1);
27
+ reject(new Error("bitquery_subscribe timed out after 15 seconds"));
28
+ }, 15e3);
29
+ this.pendingSubscribeQueue.push({
30
+ resolve,
31
+ reject,
32
+ timeout,
33
+ templateKey: params.templateKey,
34
+ variables: params.variables || {},
35
+ agentId: params.agentId,
36
+ subscriberType: params.subscriberType
37
+ });
38
+ const msg = {
39
+ type: "bitquery_subscribe",
40
+ templateKey: params.templateKey,
41
+ variables: params.variables || {},
42
+ walletId: this.config.walletId
43
+ };
44
+ if (params.agentId) {
45
+ msg.agentId = params.agentId;
46
+ msg.subscriberType = params.subscriberType || "agent";
47
+ } else if (params.subscriberType) {
48
+ msg.subscriberType = params.subscriberType;
49
+ }
50
+ try {
51
+ this.ws.send(JSON.stringify(msg));
52
+ } catch (err) {
53
+ clearTimeout(timeout);
54
+ const idx = this.pendingSubscribeQueue.findIndex((p) => p.resolve === resolve);
55
+ if (idx !== -1) this.pendingSubscribeQueue.splice(idx, 1);
56
+ reject(new Error(`Failed to send subscribe: ${err}`));
57
+ }
58
+ });
59
+ }
60
+ async unsubscribe(subscriptionId) {
61
+ this.activeSubscriptions.delete(subscriptionId);
62
+ if (!this.ws || this.ws.readyState !== 1) {
63
+ return { unsubscribed: true };
64
+ }
65
+ return new Promise((resolve) => {
66
+ const timeout = setTimeout(() => {
67
+ this.pendingUnsubscribes.delete(subscriptionId);
68
+ resolve({ unsubscribed: true });
69
+ }, 1e4);
70
+ this.pendingUnsubscribes.set(subscriptionId, { resolve, timeout });
71
+ try {
72
+ this.ws.send(JSON.stringify({ type: "bitquery_unsubscribe", subscriptionId }));
73
+ } catch {
74
+ clearTimeout(timeout);
75
+ this.pendingUnsubscribes.delete(subscriptionId);
76
+ resolve({ unsubscribed: true });
77
+ }
78
+ });
79
+ }
80
+ /** Close the WS if no active subscriptions remain. */
81
+ disconnectIfIdle() {
82
+ if (this.activeSubscriptions.size === 0) {
83
+ this.close();
84
+ }
85
+ }
86
+ close() {
87
+ this.intentionalClose = true;
88
+ if (this.reconnectTimer) {
89
+ clearTimeout(this.reconnectTimer);
90
+ this.reconnectTimer = null;
91
+ }
92
+ if (this.ws) {
93
+ try {
94
+ this.ws.close();
95
+ } catch {
96
+ }
97
+ this.ws = null;
98
+ }
99
+ this.authenticated = false;
100
+ }
101
+ async ensureConnected() {
102
+ if (this.ws && this.ws.readyState === 1 && this.authenticated) return;
103
+ if (this.connecting) {
104
+ await new Promise((resolve, reject) => {
105
+ const timeout = setTimeout(() => reject(new Error("Timed out waiting for connection")), 2e4);
106
+ const check = setInterval(() => {
107
+ if (this.authenticated) {
108
+ clearTimeout(timeout);
109
+ clearInterval(check);
110
+ resolve();
111
+ }
112
+ }, 100);
113
+ });
114
+ return;
115
+ }
116
+ this.intentionalClose = false;
117
+ this.connecting = true;
118
+ try {
119
+ await this.connect();
120
+ await new Promise((resolve, reject) => {
121
+ const timeout = setTimeout(() => reject(new Error("Authentication timed out")), 15e3);
122
+ const check = setInterval(() => {
123
+ if (this.authenticated) {
124
+ clearTimeout(timeout);
125
+ clearInterval(check);
126
+ resolve();
127
+ }
128
+ }, 100);
129
+ });
130
+ } finally {
131
+ this.connecting = false;
132
+ }
133
+ }
134
+ async connect() {
135
+ const WebSocket = (await import("ws")).default;
136
+ this.currentAccessToken = await this.config.getAccessToken();
137
+ const url = `${this.config.wsUrl}?accessToken=${encodeURIComponent(this.currentAccessToken)}`;
138
+ this.authenticated = false;
139
+ this.log("info", `Connecting to ${this.config.wsUrl}`);
140
+ return new Promise((resolve, reject) => {
141
+ let ws;
142
+ try {
143
+ ws = new WebSocket(url);
144
+ this.ws = ws;
145
+ } catch (err) {
146
+ reject(err);
147
+ return;
148
+ }
149
+ const connectTimeout = setTimeout(() => {
150
+ if (ws.readyState !== 1) {
151
+ ws.close();
152
+ reject(new Error("WS connection timed out"));
153
+ }
154
+ }, 1e4);
155
+ ws.on("open", () => {
156
+ clearTimeout(connectTimeout);
157
+ this.reconnectAttempt = 0;
158
+ this.log("info", "Connected");
159
+ resolve();
160
+ });
161
+ ws.on("message", (data) => {
162
+ try {
163
+ const msg = JSON.parse(data.toString());
164
+ this.handleMessage(msg);
165
+ } catch {
166
+ this.log("warn", "Failed to parse message");
167
+ }
168
+ });
169
+ ws.on("close", () => {
170
+ clearTimeout(connectTimeout);
171
+ this.authenticated = false;
172
+ this.log("info", "WS closed");
173
+ this.drainPendingOnClose();
174
+ if (!this.intentionalClose && this.activeSubscriptions.size > 0) {
175
+ this.scheduleReconnect();
176
+ }
177
+ });
178
+ ws.on("error", (err) => {
179
+ clearTimeout(connectTimeout);
180
+ this.log("error", `WS error: ${err.message}`);
181
+ if (ws.readyState !== 1) {
182
+ reject(err);
183
+ }
184
+ });
185
+ });
186
+ }
187
+ handleMessage(msg) {
188
+ switch (msg.type) {
189
+ case "connected":
190
+ this.log("info", "Handshake received, authenticating...");
191
+ if (this.ws && this.ws.readyState === 1) {
192
+ this.ws.send(JSON.stringify({ type: "auth", accessToken: this.currentAccessToken }));
193
+ }
194
+ break;
195
+ case "authenticated":
196
+ this.authenticated = true;
197
+ this.log("info", "Authenticated");
198
+ void this.resubscribeAll();
199
+ break;
200
+ case "bitquery_subscribed": {
201
+ const subscriptionId = msg.subscriptionId;
202
+ const streamKey = msg.streamKey;
203
+ const pending = this.pendingSubscribeQueue.shift();
204
+ if (pending) {
205
+ clearTimeout(pending.timeout);
206
+ this.activeSubscriptions.set(subscriptionId, {
207
+ subscriptionId,
208
+ templateKey: pending.templateKey,
209
+ variables: pending.variables,
210
+ agentId: pending.agentId,
211
+ subscriberType: pending.subscriberType
212
+ });
213
+ pending.resolve({ subscriptionId, streamKey });
214
+ }
215
+ break;
216
+ }
217
+ case "bitquery_unsubscribed": {
218
+ const subscriptionId = msg.subscriptionId;
219
+ this.activeSubscriptions.delete(subscriptionId);
220
+ const pending = this.pendingUnsubscribes.get(subscriptionId);
221
+ if (pending) {
222
+ clearTimeout(pending.timeout);
223
+ this.pendingUnsubscribes.delete(subscriptionId);
224
+ pending.resolve({ unsubscribed: true });
225
+ }
226
+ this.disconnectIfIdle();
227
+ break;
228
+ }
229
+ case "error": {
230
+ const code = msg.code;
231
+ this.log("error", `${code}: ${msg.message || ""}`);
232
+ if ([
233
+ "WS_SUBSCRIBE_VALIDATION_ERROR",
234
+ "BITQUERY_SUBSCRIPTION_TEMPLATE_NOT_FOUND",
235
+ "WS_SUBSCRIPTION_LIMIT_REACHED",
236
+ "WS_BRIDGE_UNAVAILABLE"
237
+ ].includes(code)) {
238
+ const pending = this.pendingSubscribeQueue.shift();
239
+ if (pending) {
240
+ clearTimeout(pending.timeout);
241
+ pending.reject(new Error(`${code}: ${msg.message || ""}`));
242
+ }
243
+ }
244
+ if (["WS_AUTH_REQUIRED", "WS_AUTH_INVALID", "ACCESS_TOKEN_EXPIRED"].includes(code)) {
245
+ this.close();
246
+ }
247
+ break;
248
+ }
249
+ }
250
+ }
251
+ drainPendingOnClose() {
252
+ for (const pending of this.pendingSubscribeQueue) {
253
+ clearTimeout(pending.timeout);
254
+ pending.reject(new Error("WebSocket closed before subscription was confirmed"));
255
+ }
256
+ this.pendingSubscribeQueue = [];
257
+ for (const [, pending] of this.pendingUnsubscribes) {
258
+ clearTimeout(pending.timeout);
259
+ pending.resolve({ unsubscribed: true });
260
+ }
261
+ this.pendingUnsubscribes.clear();
262
+ }
263
+ async resubscribeAll() {
264
+ if (this.activeSubscriptions.size === 0) return;
265
+ const subs = [...this.activeSubscriptions.values()];
266
+ this.activeSubscriptions.clear();
267
+ this.log("info", `Re-subscribing ${subs.length} subscription(s) after reconnect`);
268
+ for (const sub of subs) {
269
+ try {
270
+ const result = await this.subscribe({
271
+ templateKey: sub.templateKey,
272
+ variables: sub.variables,
273
+ agentId: sub.agentId,
274
+ subscriberType: sub.subscriberType
275
+ });
276
+ this.log("info", `Re-subscribed ${sub.templateKey} \u2192 new id: ${result.subscriptionId}`);
277
+ } catch (err) {
278
+ this.log("error", `Re-subscribe failed for ${sub.templateKey}: ${err}`);
279
+ }
280
+ }
281
+ }
282
+ scheduleReconnect() {
283
+ if (this.intentionalClose) return;
284
+ const delay = RECONNECT_DELAYS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS.length - 1)];
285
+ this.reconnectAttempt++;
286
+ this.log("info", `Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt})`);
287
+ this.reconnectTimer = setTimeout(async () => {
288
+ try {
289
+ await this.connect();
290
+ } catch (err) {
291
+ this.log("error", `Reconnect failed: ${err instanceof Error ? err.message : String(err)}`);
292
+ this.scheduleReconnect();
293
+ }
294
+ }, delay);
295
+ }
296
+ log(level, msg) {
297
+ this.config.logger?.[level](`[bitquery-ws] ${msg}`);
298
+ }
299
+ };
300
+
301
+ export {
302
+ BitqueryStreamManager
303
+ };
package/dist/index.js CHANGED
@@ -16,26 +16,28 @@ import {
16
16
  import {
17
17
  AlphaStreamManager
18
18
  } from "./chunk-3YPZOXWE.js";
19
+ import {
20
+ BitqueryStreamManager
21
+ } from "./chunk-VR5WP5S4.js";
19
22
  import {
20
23
  orchestratorRequest
21
24
  } from "./chunk-NDPVVAV7.js";
22
25
  import {
23
26
  IntelligenceLab
24
27
  } from "./chunk-FBS5FGW2.js";
25
- import {
26
- scrubUntrustedText
27
- } from "./chunk-AI6MTHUN.js";
28
- import {
29
- readRecoverySecretFromDisk
30
- } from "./chunk-SBYHSJLU.js";
31
28
  import {
32
29
  generateBulletinDigest,
33
30
  generateDecisionDigest,
34
31
  generateEntitlementsDigest,
35
- generateStateMd,
36
32
  resolveMemoryDir,
37
33
  resolveWorkspaceRoot
38
34
  } from "./chunk-JO3BXAUQ.js";
35
+ import {
36
+ scrubUntrustedText
37
+ } from "./chunk-AI6MTHUN.js";
38
+ import {
39
+ readRecoverySecretFromDisk
40
+ } from "./chunk-SBYHSJLU.js";
39
41
 
40
42
  // index.ts
41
43
  import { Type } from "@sinclair/typebox";
@@ -2206,6 +2208,16 @@ ${notes}
2206
2208
  })
2207
2209
  )
2208
2210
  });
2211
+ const bitqueryStreamManager = new BitqueryStreamManager({
2212
+ wsUrl: orchestratorUrl.replace(/^http/, "ws").replace(/\/$/, "") + "/ws",
2213
+ walletId,
2214
+ getAccessToken: () => sessionManager.getAccessToken(),
2215
+ logger: {
2216
+ info: (msg) => api.logger.info(`[solana-trader] ${msg}`),
2217
+ warn: (msg) => api.logger.warn(`[solana-trader] ${msg}`),
2218
+ error: (msg) => api.logger.error(`[solana-trader] ${msg}`)
2219
+ }
2220
+ });
2209
2221
  api.registerTool({
2210
2222
  name: "solana_bitquery_subscribe",
2211
2223
  description: "Subscribe to a managed real-time Bitquery data stream. The orchestrator manages the WebSocket connection and broadcasts events. Available templates: realtimeTokenPricesSolana, ohlc1s, dexPoolLiquidityChanges, pumpFunTokenCreation, pumpFunTrades, pumpSwapTrades, raydiumNewPools. Returns a subscriptionId for tracking. Pass agentId to enable event-to-agent forwarding \u2014 orchestrator delivers each event to your Gateway via /v1/responses in addition to normal WS delivery. Subscriptions expire after 24h and emit subscription_expiring/subscription_expired events. See websocket-streaming.md in the solana-trader skill for the full message contract and usage patterns.",
@@ -2216,18 +2228,13 @@ ${notes}
2216
2228
  subscriberType: Type.Optional(Type.Union([Type.Literal("agent"), Type.Literal("client")], { description: "Subscriber type. Inferred as 'agent' when agentId is present. Defaults to 'client'." }))
2217
2229
  }),
2218
2230
  execute: wrapExecute("solana_bitquery_subscribe", async (_id, params) => {
2219
- const body = {
2220
- templateKey: params.templateKey,
2221
- variables: params.variables || {}
2222
- };
2223
2231
  const effectiveAgentId = params.agentId || config.agentId;
2224
- if (effectiveAgentId) {
2225
- body.agentId = effectiveAgentId;
2226
- body.subscriberType = params.subscriberType || "agent";
2227
- } else if (params.subscriberType) {
2228
- body.subscriberType = params.subscriberType;
2229
- }
2230
- return post("/api/bitquery/subscribe", body);
2232
+ return bitqueryStreamManager.subscribe({
2233
+ templateKey: params.templateKey,
2234
+ variables: params.variables || {},
2235
+ agentId: effectiveAgentId,
2236
+ subscriberType: params.subscriberType || (effectiveAgentId ? "agent" : void 0)
2237
+ });
2231
2238
  })
2232
2239
  });
2233
2240
  api.registerTool({
@@ -2238,9 +2245,7 @@ ${notes}
2238
2245
  }),
2239
2246
  execute: wrapExecute(
2240
2247
  "solana_bitquery_unsubscribe",
2241
- async (_id, params) => post("/api/bitquery/unsubscribe", {
2242
- subscriptionId: params.subscriptionId
2243
- })
2248
+ async (_id, params) => bitqueryStreamManager.unsubscribe(params.subscriptionId)
2244
2249
  )
2245
2250
  });
2246
2251
  api.registerTool({
@@ -3905,7 +3910,7 @@ ${String(params.summary)}
3905
3910
  const stateFile = path.join(stateDir, `${bootAgentId}.json`);
3906
3911
  const stateData = readJsonFile(stateFile);
3907
3912
  if (stateData) {
3908
- const stateMd = generateStateMd(stateData.state || null);
3913
+ const stateMd = generateMemoryMd(bootAgentId, stateData.state || null);
3909
3914
  context.bootstrapFiles.push({
3910
3915
  name: `${bootAgentId}-state.md`,
3911
3916
  path: `state/${bootAgentId}-state.md`,
@@ -4124,7 +4129,7 @@ Context compaction triggered. STATE.md synced from last persisted state. Decisio
4124
4129
  const stateFile = path.join(stateDir, `${assembleAgentId}.json`);
4125
4130
  const stateData = readJsonFile(stateFile);
4126
4131
  if (stateData?.state) {
4127
- lines.push(generateStateMd(stateData.state));
4132
+ lines.push(generateMemoryMd(assembleAgentId, stateData.state));
4128
4133
  }
4129
4134
  } catch {
4130
4135
  }
@@ -0,0 +1,6 @@
1
+ import {
2
+ BitqueryStreamManager
3
+ } from "../chunk-VR5WP5S4.js";
4
+ export {
5
+ BitqueryStreamManager
6
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solana-traderclaw",
3
- "version": "1.0.89",
3
+ "version": "1.0.91",
4
4
  "description": "TraderClaw V1-Upgraded — Solana trading for OpenClaw with intelligence lab, tool envelopes, prompt scrubbing, read-only X social intel, and split skill docs",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",