@dimcool/dimclaw 0.1.14 → 0.1.18

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/dim-client.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  * Handles wallet-based authentication using a Solana keypair.
4
4
  */
5
5
 
6
+ import { mkdir, writeFile, rename } from 'node:fs/promises';
7
+ import path from 'node:path';
6
8
  import { SDK, NodeStorage } from '@dimcool/sdk';
7
9
  import { Keypair, Transaction } from '@solana/web3.js';
8
10
  import bs58 from 'bs58';
@@ -15,6 +17,14 @@ export interface DimClientConfig {
15
17
  apiUrl?: string;
16
18
  /** Referral code to use on first signup */
17
19
  referralCode?: string;
20
+ /** Path to HEARTBEAT.md for OpenClaw's heartbeat cycle */
21
+ heartbeatPath?: string;
22
+ }
23
+
24
+ export interface BufferedEvent {
25
+ event: string;
26
+ payload: unknown;
27
+ at: string;
18
28
  }
19
29
 
20
30
  export class DimClient {
@@ -23,6 +33,8 @@ export class DimClient {
23
33
  private config: DimClientConfig;
24
34
  private authenticated = false;
25
35
  private userId: string | null = null;
36
+ private eventQueue: BufferedEvent[] = [];
37
+ private unsubscribers: Array<() => void> = [];
26
38
 
27
39
  constructor(config: DimClientConfig) {
28
40
  this.config = config;
@@ -96,6 +108,106 @@ export class DimClient {
96
108
  await this.sdk.ensureWebSocketConnected(timeoutMs);
97
109
  }
98
110
 
111
+ /**
112
+ * Subscribe to key WS events and buffer them for agent consumption.
113
+ * Call after authenticate() when the WS transport is connected.
114
+ */
115
+ startEventListeners(): void {
116
+ const events = [
117
+ 'chat:message',
118
+ 'notification',
119
+ 'lobby:matched',
120
+ 'lobby:invitation',
121
+ 'game:turn',
122
+ 'game:completed',
123
+ ];
124
+ for (const event of events) {
125
+ this.unsubscribers.push(
126
+ this.sdk.events.subscribe(event, (payload: unknown) => {
127
+ this.eventQueue.push({
128
+ event,
129
+ payload,
130
+ at: new Date().toISOString(),
131
+ });
132
+ this.writeHeartbeat().catch(() => {});
133
+ }),
134
+ );
135
+ }
136
+ }
137
+
138
+ /** Write HEARTBEAT.md with pending event summary for OpenClaw's heartbeat cycle. */
139
+ private async writeHeartbeat(): Promise<void> {
140
+ if (!this.config.heartbeatPath) return;
141
+ const filePath = this.resolveHeartbeatPath();
142
+ const count = this.eventQueue.length;
143
+ if (count === 0) return;
144
+ const lines = ['# DIM Heartbeat', ''];
145
+ const eventTypes = new Set(this.eventQueue.map((e) => e.event));
146
+ if (eventTypes.has('chat:message')) {
147
+ lines.push('- You have new DMs — call dim_check_notifications');
148
+ }
149
+ if (eventTypes.has('notification')) {
150
+ lines.push(
151
+ '- New notifications (challenges, friend requests, game results) — call dim_check_notifications',
152
+ );
153
+ }
154
+ if (eventTypes.has('lobby:matched') || eventTypes.has('lobby:invitation')) {
155
+ lines.push('- A game match is ready — call dim_get_pending_events');
156
+ }
157
+ if (eventTypes.has('game:turn')) {
158
+ lines.push("- It's your turn in a game — call dim_get_pending_events");
159
+ }
160
+ if (eventTypes.has('game:completed')) {
161
+ lines.push('- A game has completed — call dim_get_pending_events');
162
+ }
163
+ lines.push('');
164
+ await mkdir(path.dirname(filePath), { recursive: true });
165
+ const tmp = `${filePath}.tmp`;
166
+ await writeFile(tmp, lines.join('\n'), 'utf8');
167
+ await rename(tmp, filePath);
168
+ }
169
+
170
+ private resolveHeartbeatPath(): string {
171
+ const p = this.config.heartbeatPath!;
172
+ if (p.startsWith('~/') && process.env.HOME) {
173
+ return path.join(process.env.HOME, p.slice(2));
174
+ }
175
+ return path.resolve(p);
176
+ }
177
+
178
+ /** Drain all buffered events since last call. */
179
+ drainEvents(): BufferedEvent[] {
180
+ return this.eventQueue.splice(0);
181
+ }
182
+
183
+ /** Peek at buffered event count without draining. */
184
+ get pendingEventCount(): number {
185
+ return this.eventQueue.length;
186
+ }
187
+
188
+ // ── Daily spend tracking ──────────────────────────────────────────────
189
+
190
+ private dailySpendMinor = 0;
191
+ private spendResetDate = '';
192
+
193
+ private resetDailySpendIfNeeded(): void {
194
+ const today = new Date().toISOString().slice(0, 10);
195
+ if (this.spendResetDate !== today) {
196
+ this.dailySpendMinor = 0;
197
+ this.spendResetDate = today;
198
+ }
199
+ }
200
+
201
+ recordSpend(amountMinor: number): void {
202
+ this.resetDailySpendIfNeeded();
203
+ this.dailySpendMinor += amountMinor;
204
+ }
205
+
206
+ get dailySpentDollars(): number {
207
+ this.resetDailySpendIfNeeded();
208
+ return this.dailySpendMinor / 1_000_000;
209
+ }
210
+
99
211
  getKeypair(): Keypair {
100
212
  return this.keypair;
101
213
  }