@bytespell/amux-client 0.0.19
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/accumulator.d.ts +77 -0
- package/dist/accumulator.d.ts.map +1 -0
- package/dist/accumulator.js +285 -0
- package/dist/accumulator.js.map +1 -0
- package/dist/client.d.ts +155 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +364 -0
- package/dist/client.js.map +1 -0
- package/dist/connection.d.ts +81 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +143 -0
- package/dist/connection.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/react/context.d.ts +60 -0
- package/dist/react/context.d.ts.map +1 -0
- package/dist/react/context.js +138 -0
- package/dist/react/context.js.map +1 -0
- package/dist/react/hooks.d.ts +153 -0
- package/dist/react/hooks.d.ts.map +1 -0
- package/dist/react/hooks.js +156 -0
- package/dist/react/hooks.js.map +1 -0
- package/dist/react/index.d.ts +29 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +27 -0
- package/dist/react/index.js.map +1 -0
- package/dist/types.d.ts +166 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +48 -0
- package/src/accumulator.ts +307 -0
- package/src/client.ts +445 -0
- package/src/connection.ts +194 -0
- package/src/index.ts +59 -0
- package/src/react/context.tsx +200 -0
- package/src/react/hooks.ts +208 -0
- package/src/react/index.ts +37 -0
- package/src/types.ts +120 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import type { ServerMessage, AgentInfo, ModelInfo, SessionMetadata } from '@bytespell/amux-types';
|
|
2
|
+
import { Connection } from './connection.js';
|
|
3
|
+
import { Accumulator } from './accumulator.js';
|
|
4
|
+
import type {
|
|
5
|
+
AmuxClientOptions,
|
|
6
|
+
AmuxClientEvents,
|
|
7
|
+
ConnectionStatus,
|
|
8
|
+
Message,
|
|
9
|
+
Turn,
|
|
10
|
+
PermissionOptionData,
|
|
11
|
+
} from './types.js';
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
type EventHandler = (data: any) => void;
|
|
15
|
+
type EventHandlers = Map<keyof AmuxClientEvents, Set<EventHandler>>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* AmuxClient - WebSocket client for consuming amux
|
|
19
|
+
*
|
|
20
|
+
* Provides:
|
|
21
|
+
* - Connection management with auto-reconnect
|
|
22
|
+
* - Type-safe event subscription
|
|
23
|
+
* - Command methods for server interaction
|
|
24
|
+
* - Message accumulation for UI rendering
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const client = new AmuxClient({ url: 'ws://localhost:3000/ws' });
|
|
29
|
+
*
|
|
30
|
+
* client.on('ready', (data) => console.log('Ready:', data.cwd));
|
|
31
|
+
* client.on('update', (update) => console.log('Update:', update));
|
|
32
|
+
*
|
|
33
|
+
* client.connect();
|
|
34
|
+
* client.prompt('Hello!');
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export class AmuxClient {
|
|
38
|
+
private connection: Connection;
|
|
39
|
+
private accumulator: Accumulator;
|
|
40
|
+
private handlers: EventHandlers = new Map();
|
|
41
|
+
|
|
42
|
+
// Session state (updated from events)
|
|
43
|
+
private _cwd: string | null = null;
|
|
44
|
+
private _sessionId: string | null = null;
|
|
45
|
+
private _agent: AgentInfo | null = null;
|
|
46
|
+
private _availableAgents: Array<{ id: string; name: string }> = [];
|
|
47
|
+
private _availableModels: ModelInfo[] = [];
|
|
48
|
+
private _currentModelId: string | null = null;
|
|
49
|
+
private _capabilities: unknown = null;
|
|
50
|
+
private _sessions: SessionMetadata[] = [];
|
|
51
|
+
private _isReady = false;
|
|
52
|
+
private _isProcessing = false;
|
|
53
|
+
private _pendingPermission: AmuxClientEvents['permission_request'] | null = null;
|
|
54
|
+
|
|
55
|
+
constructor(options: AmuxClientOptions) {
|
|
56
|
+
this.connection = new Connection({
|
|
57
|
+
url: options.url,
|
|
58
|
+
reconnect: options.reconnect ?? true,
|
|
59
|
+
reconnectInterval: options.reconnectInterval ?? 1000,
|
|
60
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? Infinity,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
this.accumulator = new Accumulator();
|
|
64
|
+
|
|
65
|
+
// Wire up connection events
|
|
66
|
+
this.connection.on('message', (msg) => this.handleMessage(msg));
|
|
67
|
+
this.connection.on('status', (status) => this.handleStatusChange(status));
|
|
68
|
+
this.connection.on('close', () => {
|
|
69
|
+
this._isReady = false;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Auto-connect if enabled
|
|
73
|
+
if (options.autoConnect !== false) {
|
|
74
|
+
this.connect();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ============================================================
|
|
79
|
+
// Connection lifecycle
|
|
80
|
+
// ============================================================
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Connect to the WebSocket server
|
|
84
|
+
*/
|
|
85
|
+
connect(): void {
|
|
86
|
+
this.connection.connect();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Disconnect from the WebSocket server
|
|
91
|
+
*/
|
|
92
|
+
disconnect(): void {
|
|
93
|
+
this.connection.disconnect();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Whether WebSocket is connected
|
|
98
|
+
*/
|
|
99
|
+
get isConnected(): boolean {
|
|
100
|
+
return this.connection.isConnected;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Whether client is ready (connected and received 'ready' event)
|
|
105
|
+
*/
|
|
106
|
+
get isReady(): boolean {
|
|
107
|
+
return this._isReady;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Current connection status
|
|
112
|
+
*/
|
|
113
|
+
get connectionStatus(): ConnectionStatus {
|
|
114
|
+
return this.connection.status;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ============================================================
|
|
118
|
+
// Event subscription
|
|
119
|
+
// ============================================================
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Subscribe to an event
|
|
123
|
+
*/
|
|
124
|
+
on<K extends keyof AmuxClientEvents>(
|
|
125
|
+
event: K,
|
|
126
|
+
handler: (data: AmuxClientEvents[K]) => void
|
|
127
|
+
): void {
|
|
128
|
+
if (!this.handlers.has(event)) {
|
|
129
|
+
this.handlers.set(event, new Set());
|
|
130
|
+
}
|
|
131
|
+
this.handlers.get(event)!.add(handler as EventHandler);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Unsubscribe from an event
|
|
136
|
+
*/
|
|
137
|
+
off<K extends keyof AmuxClientEvents>(
|
|
138
|
+
event: K,
|
|
139
|
+
handler: (data: AmuxClientEvents[K]) => void
|
|
140
|
+
): void {
|
|
141
|
+
this.handlers.get(event)?.delete(handler as EventHandler);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Emit an event to handlers
|
|
146
|
+
*/
|
|
147
|
+
private emit<K extends keyof AmuxClientEvents>(event: K, data: AmuxClientEvents[K]): void {
|
|
148
|
+
const handlers = this.handlers.get(event);
|
|
149
|
+
if (handlers) {
|
|
150
|
+
for (const handler of handlers) {
|
|
151
|
+
handler(data);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ============================================================
|
|
157
|
+
// Session state (read-only)
|
|
158
|
+
// ============================================================
|
|
159
|
+
|
|
160
|
+
/** Current working directory */
|
|
161
|
+
get cwd(): string | null {
|
|
162
|
+
return this._cwd;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Current session ID */
|
|
166
|
+
get sessionId(): string | null {
|
|
167
|
+
return this._sessionId;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Current agent info */
|
|
171
|
+
get agent(): AgentInfo | null {
|
|
172
|
+
return this._agent;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Available agents */
|
|
176
|
+
get availableAgents(): Array<{ id: string; name: string }> {
|
|
177
|
+
return this._availableAgents;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Available models */
|
|
181
|
+
get availableModels(): ModelInfo[] {
|
|
182
|
+
return this._availableModels;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Current model ID */
|
|
186
|
+
get currentModelId(): string | null {
|
|
187
|
+
return this._currentModelId;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Agent capabilities */
|
|
191
|
+
get capabilities(): unknown {
|
|
192
|
+
return this._capabilities;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** List of sessions (from list_sessions response) */
|
|
196
|
+
get sessions(): SessionMetadata[] {
|
|
197
|
+
return this._sessions;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ============================================================
|
|
201
|
+
// Derived state (computed)
|
|
202
|
+
// ============================================================
|
|
203
|
+
|
|
204
|
+
/** Whether currently processing a turn */
|
|
205
|
+
get isProcessing(): boolean {
|
|
206
|
+
return this._isProcessing;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Whether agent is actively streaming */
|
|
210
|
+
get isStreaming(): boolean {
|
|
211
|
+
return this.accumulator.currentTurn?.status === 'streaming';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Whether blocked on permission request */
|
|
215
|
+
get isAwaitingPermission(): boolean {
|
|
216
|
+
return this._pendingPermission !== null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Current pending permission request */
|
|
220
|
+
get pendingPermission(): AmuxClientEvents['permission_request'] | null {
|
|
221
|
+
return this._pendingPermission;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ============================================================
|
|
225
|
+
// Message accumulator access
|
|
226
|
+
// ============================================================
|
|
227
|
+
|
|
228
|
+
/** All accumulated messages */
|
|
229
|
+
get messages(): Message[] {
|
|
230
|
+
return this.accumulator.messages;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** All turns */
|
|
234
|
+
get turns(): Turn[] {
|
|
235
|
+
return this.accumulator.turns;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Current active turn */
|
|
239
|
+
get currentTurn(): Turn | null {
|
|
240
|
+
return this.accumulator.currentTurn;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Last user message */
|
|
244
|
+
get lastUserMessage(): Message | null {
|
|
245
|
+
return this.accumulator.lastUserMessage;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Last assistant message */
|
|
249
|
+
get lastAssistantMessage(): Message | null {
|
|
250
|
+
return this.accumulator.lastAssistantMessage;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ============================================================
|
|
254
|
+
// Commands (fire-and-forget)
|
|
255
|
+
// ============================================================
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Send a prompt to the agent
|
|
259
|
+
*/
|
|
260
|
+
prompt(message: string): void {
|
|
261
|
+
this.connection.send({ type: 'prompt', message });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Cancel current operation
|
|
266
|
+
*/
|
|
267
|
+
cancel(): void {
|
|
268
|
+
this.connection.send({ type: 'cancel' });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Respond to a permission request
|
|
273
|
+
*/
|
|
274
|
+
respondToPermission(requestId: string, optionId: string): void {
|
|
275
|
+
this.connection.send({ type: 'permission_response', requestId, optionId });
|
|
276
|
+
this._pendingPermission = null;
|
|
277
|
+
this.accumulator.resumeStreaming();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Change working directory
|
|
282
|
+
*/
|
|
283
|
+
changeCwd(path: string): void {
|
|
284
|
+
this.connection.send({ type: 'change_cwd', path });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Create a new session
|
|
289
|
+
*/
|
|
290
|
+
newSession(): void {
|
|
291
|
+
this.connection.send({ type: 'new_session' });
|
|
292
|
+
this.accumulator.clear();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Change agent type
|
|
297
|
+
*/
|
|
298
|
+
changeAgent(agentType: string): void {
|
|
299
|
+
this.connection.send({ type: 'change_agent', agentType });
|
|
300
|
+
this.accumulator.clear();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Set session mode
|
|
305
|
+
*/
|
|
306
|
+
setMode(modeId: string): void {
|
|
307
|
+
this.connection.send({ type: 'set_mode', modeId });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Set model
|
|
312
|
+
*/
|
|
313
|
+
setModel(modelId: string): void {
|
|
314
|
+
this.connection.send({ type: 'set_model', modelId });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Request list of sessions
|
|
319
|
+
*/
|
|
320
|
+
requestSessions(): void {
|
|
321
|
+
this.connection.send({ type: 'list_sessions' });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Switch to a different session
|
|
326
|
+
*/
|
|
327
|
+
switchSession(sessionId: string): void {
|
|
328
|
+
this.connection.send({ type: 'switch_session', sessionId });
|
|
329
|
+
this.accumulator.clear();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Request history for current session
|
|
334
|
+
*/
|
|
335
|
+
requestHistory(): void {
|
|
336
|
+
this.connection.send({ type: 'get_history' });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ============================================================
|
|
340
|
+
// Internal message handling
|
|
341
|
+
// ============================================================
|
|
342
|
+
|
|
343
|
+
private handleMessage(msg: ServerMessage): void {
|
|
344
|
+
switch (msg.type) {
|
|
345
|
+
case 'ready':
|
|
346
|
+
this._isReady = true;
|
|
347
|
+
this._cwd = msg.cwd;
|
|
348
|
+
this._sessionId = msg.sessionId;
|
|
349
|
+
this._agent = msg.agent;
|
|
350
|
+
this._availableAgents = msg.availableAgents;
|
|
351
|
+
this._availableModels = msg.availableModels ?? [];
|
|
352
|
+
this._currentModelId = msg.currentModelId ?? null;
|
|
353
|
+
this._capabilities = msg.capabilities;
|
|
354
|
+
this.emit('ready', msg);
|
|
355
|
+
break;
|
|
356
|
+
|
|
357
|
+
case 'connecting':
|
|
358
|
+
this._isReady = false;
|
|
359
|
+
this.emit('connecting', {});
|
|
360
|
+
break;
|
|
361
|
+
|
|
362
|
+
case 'session_update':
|
|
363
|
+
this.accumulator.processUpdate(msg.update);
|
|
364
|
+
this.emit('update', msg.update);
|
|
365
|
+
this.emit('messages_updated', { messages: this.messages });
|
|
366
|
+
this.emit('turns_updated', { turns: this.turns });
|
|
367
|
+
break;
|
|
368
|
+
|
|
369
|
+
case 'turn_start':
|
|
370
|
+
this._isProcessing = true;
|
|
371
|
+
this.accumulator.startTurn();
|
|
372
|
+
this.emit('turn_start', {});
|
|
373
|
+
break;
|
|
374
|
+
|
|
375
|
+
case 'turn_end':
|
|
376
|
+
this._isProcessing = false;
|
|
377
|
+
this.accumulator.endTurn();
|
|
378
|
+
this.emit('turn_end', {});
|
|
379
|
+
this.emit('messages_updated', { messages: this.messages });
|
|
380
|
+
this.emit('turns_updated', { turns: this.turns });
|
|
381
|
+
break;
|
|
382
|
+
|
|
383
|
+
case 'permission_request': {
|
|
384
|
+
const permissionData = {
|
|
385
|
+
requestId: msg.requestId,
|
|
386
|
+
toolCall: msg.toolCall,
|
|
387
|
+
options: msg.options as unknown as PermissionOptionData[],
|
|
388
|
+
};
|
|
389
|
+
this._pendingPermission = permissionData;
|
|
390
|
+
this.accumulator.setAwaitingPermission();
|
|
391
|
+
this.emit('permission_request', permissionData);
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
case 'prompt_complete':
|
|
396
|
+
this.emit('prompt_complete', msg);
|
|
397
|
+
break;
|
|
398
|
+
|
|
399
|
+
case 'session_created':
|
|
400
|
+
this._sessionId = msg.sessionId;
|
|
401
|
+
this.emit('session_created', msg);
|
|
402
|
+
break;
|
|
403
|
+
|
|
404
|
+
case 'session_switched':
|
|
405
|
+
this._sessionId = msg.sessionId;
|
|
406
|
+
this.emit('session_switched', msg);
|
|
407
|
+
break;
|
|
408
|
+
|
|
409
|
+
case 'history_replay':
|
|
410
|
+
this.accumulator.replayHistory(msg.events);
|
|
411
|
+
this.emit('history_replay', msg);
|
|
412
|
+
this.emit('messages_updated', { messages: this.messages });
|
|
413
|
+
this.emit('turns_updated', { turns: this.turns });
|
|
414
|
+
break;
|
|
415
|
+
|
|
416
|
+
case 'history':
|
|
417
|
+
this.emit('history', msg);
|
|
418
|
+
break;
|
|
419
|
+
|
|
420
|
+
case 'sessions':
|
|
421
|
+
this._sessions = msg.sessions;
|
|
422
|
+
this.emit('sessions', msg);
|
|
423
|
+
break;
|
|
424
|
+
|
|
425
|
+
case 'error':
|
|
426
|
+
this.emit('error', msg);
|
|
427
|
+
break;
|
|
428
|
+
|
|
429
|
+
case 'agent_exit':
|
|
430
|
+
this._isReady = false;
|
|
431
|
+
this._isProcessing = false;
|
|
432
|
+
this.emit('agent_exit', msg);
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private handleStatusChange(status: ConnectionStatus): void {
|
|
438
|
+
if (status === 'ready') {
|
|
439
|
+
this._isReady = true;
|
|
440
|
+
} else if (status === 'disconnected') {
|
|
441
|
+
this._isReady = false;
|
|
442
|
+
}
|
|
443
|
+
this.emit('connection_status', { status });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { ClientMessage, ServerMessage } from '@bytespell/amux-types';
|
|
2
|
+
import type { ConnectionStatus } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Event handler types for Connection
|
|
6
|
+
*/
|
|
7
|
+
export interface ConnectionEvents {
|
|
8
|
+
open: () => void;
|
|
9
|
+
close: (event: { code: number; reason: string; wasClean: boolean }) => void;
|
|
10
|
+
message: (message: ServerMessage) => void;
|
|
11
|
+
error: (error: Event) => void;
|
|
12
|
+
status: (status: ConnectionStatus) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Connection options
|
|
17
|
+
*/
|
|
18
|
+
export interface ConnectionOptions {
|
|
19
|
+
url: string;
|
|
20
|
+
reconnect?: boolean;
|
|
21
|
+
reconnectInterval?: number;
|
|
22
|
+
maxReconnectAttempts?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
type EventHandler = (...args: any[]) => void;
|
|
27
|
+
type HandlerMap = Map<keyof ConnectionEvents, Set<EventHandler>>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* WebSocket connection manager with auto-reconnect
|
|
31
|
+
*/
|
|
32
|
+
export class Connection {
|
|
33
|
+
private ws: WebSocket | null = null;
|
|
34
|
+
private reconnectAttempts = 0;
|
|
35
|
+
private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
36
|
+
private _status: ConnectionStatus = 'disconnected';
|
|
37
|
+
private handlers: HandlerMap = new Map();
|
|
38
|
+
|
|
39
|
+
private readonly url: string;
|
|
40
|
+
private readonly reconnect: boolean;
|
|
41
|
+
private readonly reconnectInterval: number;
|
|
42
|
+
private readonly maxReconnectAttempts: number;
|
|
43
|
+
|
|
44
|
+
constructor(options: ConnectionOptions) {
|
|
45
|
+
this.url = options.url;
|
|
46
|
+
this.reconnect = options.reconnect ?? true;
|
|
47
|
+
this.reconnectInterval = options.reconnectInterval ?? 1000;
|
|
48
|
+
this.maxReconnectAttempts = options.maxReconnectAttempts ?? Infinity;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Current connection status
|
|
53
|
+
*/
|
|
54
|
+
get status(): ConnectionStatus {
|
|
55
|
+
return this._status;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Whether the connection is open
|
|
60
|
+
*/
|
|
61
|
+
get isConnected(): boolean {
|
|
62
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Add event listener
|
|
67
|
+
*/
|
|
68
|
+
on<K extends keyof ConnectionEvents>(event: K, handler: ConnectionEvents[K]): void {
|
|
69
|
+
if (!this.handlers.has(event)) {
|
|
70
|
+
this.handlers.set(event, new Set());
|
|
71
|
+
}
|
|
72
|
+
this.handlers.get(event)!.add(handler as EventHandler);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Remove event listener
|
|
77
|
+
*/
|
|
78
|
+
off<K extends keyof ConnectionEvents>(event: K, handler: ConnectionEvents[K]): void {
|
|
79
|
+
this.handlers.get(event)?.delete(handler as EventHandler);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Emit event to handlers
|
|
84
|
+
*/
|
|
85
|
+
private emit<K extends keyof ConnectionEvents>(
|
|
86
|
+
event: K,
|
|
87
|
+
...args: Parameters<ConnectionEvents[K]>
|
|
88
|
+
): void {
|
|
89
|
+
const handlers = this.handlers.get(event);
|
|
90
|
+
if (handlers) {
|
|
91
|
+
for (const handler of handlers) {
|
|
92
|
+
handler(...args);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Set and emit status change
|
|
99
|
+
*/
|
|
100
|
+
private setStatus(status: ConnectionStatus): void {
|
|
101
|
+
if (this._status !== status) {
|
|
102
|
+
this._status = status;
|
|
103
|
+
this.emit('status', status);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Connect to WebSocket server
|
|
109
|
+
*/
|
|
110
|
+
connect(): void {
|
|
111
|
+
if (this.ws && (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.setStatus('connecting');
|
|
116
|
+
this.ws = new WebSocket(this.url);
|
|
117
|
+
|
|
118
|
+
this.ws.onopen = () => {
|
|
119
|
+
this.reconnectAttempts = 0;
|
|
120
|
+
this.setStatus('connected');
|
|
121
|
+
this.emit('open');
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
this.ws.onclose = (event) => {
|
|
125
|
+
this.setStatus('disconnected');
|
|
126
|
+
this.emit('close', { code: event.code, reason: event.reason, wasClean: event.wasClean });
|
|
127
|
+
this.ws = null;
|
|
128
|
+
|
|
129
|
+
if (this.reconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
130
|
+
this.scheduleReconnect();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
this.ws.onerror = (error) => {
|
|
135
|
+
this.emit('error', error);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
this.ws.onmessage = (event) => {
|
|
139
|
+
try {
|
|
140
|
+
const message = JSON.parse(event.data as string) as ServerMessage;
|
|
141
|
+
this.emit('message', message);
|
|
142
|
+
} catch {
|
|
143
|
+
console.error('[amux-client] Failed to parse message:', event.data);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Disconnect from WebSocket server
|
|
150
|
+
*/
|
|
151
|
+
disconnect(): void {
|
|
152
|
+
if (this.reconnectTimeout) {
|
|
153
|
+
clearTimeout(this.reconnectTimeout);
|
|
154
|
+
this.reconnectTimeout = null;
|
|
155
|
+
}
|
|
156
|
+
this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnect
|
|
157
|
+
if (this.ws) {
|
|
158
|
+
this.ws.close();
|
|
159
|
+
this.ws = null;
|
|
160
|
+
}
|
|
161
|
+
this.setStatus('disconnected');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Send a message to the server
|
|
166
|
+
*/
|
|
167
|
+
send(message: ClientMessage): void {
|
|
168
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
169
|
+
console.warn('[amux-client] Cannot send message: not connected');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.ws.send(JSON.stringify(message));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Schedule a reconnection attempt
|
|
177
|
+
*/
|
|
178
|
+
private scheduleReconnect(): void {
|
|
179
|
+
if (this.reconnectTimeout) return;
|
|
180
|
+
|
|
181
|
+
this.reconnectAttempts++;
|
|
182
|
+
const delay = Math.min(
|
|
183
|
+
this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1),
|
|
184
|
+
30000 // Max 30 seconds
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
console.log(`[amux-client] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
188
|
+
|
|
189
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
190
|
+
this.reconnectTimeout = null;
|
|
191
|
+
this.connect();
|
|
192
|
+
}, delay);
|
|
193
|
+
}
|
|
194
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bytespell/amux-client
|
|
3
|
+
*
|
|
4
|
+
* Client library for consuming amux over WebSocket.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { AmuxClient } from '@bytespell/amux-client';
|
|
9
|
+
*
|
|
10
|
+
* const client = new AmuxClient({ url: 'ws://localhost:3000/ws' });
|
|
11
|
+
*
|
|
12
|
+
* client.on('ready', (data) => {
|
|
13
|
+
* console.log('Ready:', data.cwd);
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* client.on('update', (update) => {
|
|
17
|
+
* console.log('Update:', update);
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* client.connect();
|
|
21
|
+
* client.prompt('Hello!');
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* For React integration, use:
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import { AmuxProvider, useAmux } from '@bytespell/amux-client/react';
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// Main client
|
|
31
|
+
export { AmuxClient } from './client.js';
|
|
32
|
+
|
|
33
|
+
// Supporting classes
|
|
34
|
+
export { Connection } from './connection.js';
|
|
35
|
+
export type { ConnectionOptions, ConnectionEvents } from './connection.js';
|
|
36
|
+
|
|
37
|
+
export { Accumulator } from './accumulator.js';
|
|
38
|
+
|
|
39
|
+
// Client types
|
|
40
|
+
export type {
|
|
41
|
+
AmuxClientOptions,
|
|
42
|
+
AmuxClientEvents,
|
|
43
|
+
ConnectionStatus,
|
|
44
|
+
Message,
|
|
45
|
+
Turn,
|
|
46
|
+
ToolCallState,
|
|
47
|
+
ContentBlock,
|
|
48
|
+
} from './types.js';
|
|
49
|
+
|
|
50
|
+
// Re-export shared types for convenience
|
|
51
|
+
export type {
|
|
52
|
+
ServerMessage,
|
|
53
|
+
ClientMessage,
|
|
54
|
+
SessionUpdate,
|
|
55
|
+
AgentInfo,
|
|
56
|
+
ModelInfo,
|
|
57
|
+
SessionMetadata,
|
|
58
|
+
PermissionRequest,
|
|
59
|
+
} from '@bytespell/amux-types';
|