@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.
Files changed (43) hide show
  1. package/dist/accumulator.d.ts +77 -0
  2. package/dist/accumulator.d.ts.map +1 -0
  3. package/dist/accumulator.js +285 -0
  4. package/dist/accumulator.js.map +1 -0
  5. package/dist/client.d.ts +155 -0
  6. package/dist/client.d.ts.map +1 -0
  7. package/dist/client.js +364 -0
  8. package/dist/client.js.map +1 -0
  9. package/dist/connection.d.ts +81 -0
  10. package/dist/connection.d.ts.map +1 -0
  11. package/dist/connection.js +143 -0
  12. package/dist/connection.js.map +1 -0
  13. package/dist/index.d.ts +35 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +34 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/react/context.d.ts +60 -0
  18. package/dist/react/context.d.ts.map +1 -0
  19. package/dist/react/context.js +138 -0
  20. package/dist/react/context.js.map +1 -0
  21. package/dist/react/hooks.d.ts +153 -0
  22. package/dist/react/hooks.d.ts.map +1 -0
  23. package/dist/react/hooks.js +156 -0
  24. package/dist/react/hooks.js.map +1 -0
  25. package/dist/react/index.d.ts +29 -0
  26. package/dist/react/index.d.ts.map +1 -0
  27. package/dist/react/index.js +27 -0
  28. package/dist/react/index.js.map +1 -0
  29. package/dist/types.d.ts +166 -0
  30. package/dist/types.d.ts.map +1 -0
  31. package/dist/types.js +2 -0
  32. package/dist/types.js.map +1 -0
  33. package/package.json +48 -0
  34. package/src/accumulator.ts +307 -0
  35. package/src/client.ts +445 -0
  36. package/src/connection.ts +194 -0
  37. package/src/index.ts +59 -0
  38. package/src/react/context.tsx +200 -0
  39. package/src/react/hooks.ts +208 -0
  40. package/src/react/index.ts +37 -0
  41. package/src/types.ts +120 -0
  42. package/tsconfig.json +14 -0
  43. 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';