@cmdop/react 0.1.1

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/index.js ADDED
@@ -0,0 +1,732 @@
1
+ "use client";
2
+
3
+ // src/index.tsx
4
+ import {
5
+ DEFAULT_CONFIG,
6
+ VERSION,
7
+ CMDOPError,
8
+ ConnectionError,
9
+ AuthenticationError,
10
+ SessionError,
11
+ TimeoutError,
12
+ NotFoundError,
13
+ PermissionError,
14
+ ResourceExhaustedError,
15
+ CancelledError,
16
+ UnavailableError,
17
+ api,
18
+ machines,
19
+ workspaces,
20
+ system,
21
+ API_BASE_URL,
22
+ MachinesModule,
23
+ WorkspacesModule,
24
+ SystemModule
25
+ } from "@cmdop/core";
26
+
27
+ // src/centrifugo/client.ts
28
+ import { Centrifuge } from "centrifuge";
29
+ var CMDOPWebSocketClient = class {
30
+ constructor(config) {
31
+ this.config = config;
32
+ }
33
+ centrifuge = null;
34
+ subscriptions = /* @__PURE__ */ new Map();
35
+ stateListeners = /* @__PURE__ */ new Set();
36
+ state = {
37
+ isConnected: false,
38
+ isConnecting: false,
39
+ error: null
40
+ };
41
+ /**
42
+ * Connect to Centrifugo server
43
+ */
44
+ async connect() {
45
+ if (this.centrifuge) {
46
+ return;
47
+ }
48
+ this.updateState({ isConnecting: true, error: null });
49
+ try {
50
+ const token = await this.config.getToken();
51
+ this.centrifuge = new Centrifuge(this.config.url, {
52
+ token,
53
+ getToken: async () => {
54
+ return this.config.getToken();
55
+ },
56
+ debug: this.config.debug ?? false
57
+ });
58
+ this.centrifuge.on("connected", () => {
59
+ this.updateState({ isConnected: true, isConnecting: false, error: null });
60
+ });
61
+ this.centrifuge.on("disconnected", () => {
62
+ this.updateState({ isConnected: false, isConnecting: false });
63
+ });
64
+ this.centrifuge.on("error", (ctx) => {
65
+ this.updateState({ error: new Error(ctx.error?.message ?? "Connection error") });
66
+ });
67
+ this.centrifuge.connect();
68
+ } catch (error) {
69
+ this.updateState({
70
+ isConnecting: false,
71
+ error: error instanceof Error ? error : new Error(String(error))
72
+ });
73
+ throw error;
74
+ }
75
+ }
76
+ /**
77
+ * Disconnect from server
78
+ */
79
+ disconnect() {
80
+ if (this.centrifuge) {
81
+ this.subscriptions.forEach((sub) => sub.unsubscribe());
82
+ this.subscriptions.clear();
83
+ this.centrifuge.disconnect();
84
+ this.centrifuge = null;
85
+ this.updateState({ isConnected: false, isConnecting: false });
86
+ }
87
+ }
88
+ /**
89
+ * Subscribe to a channel
90
+ */
91
+ subscribe(channel, onPublication, onError) {
92
+ if (!this.centrifuge) {
93
+ throw new Error("Not connected");
94
+ }
95
+ if (this.subscriptions.has(channel)) {
96
+ const existing = this.subscriptions.get(channel);
97
+ existing.on("publication", (ctx) => {
98
+ onPublication(ctx.data);
99
+ });
100
+ return () => this.unsubscribe(channel);
101
+ }
102
+ const subscription = this.centrifuge.newSubscription(channel);
103
+ subscription.on("publication", (ctx) => {
104
+ onPublication(ctx.data);
105
+ });
106
+ subscription.on("error", (ctx) => {
107
+ onError?.(new Error(ctx.error?.message ?? "Subscription error"));
108
+ });
109
+ subscription.subscribe();
110
+ this.subscriptions.set(channel, subscription);
111
+ return () => this.unsubscribe(channel);
112
+ }
113
+ /**
114
+ * Unsubscribe from a channel
115
+ */
116
+ unsubscribe(channel) {
117
+ const subscription = this.subscriptions.get(channel);
118
+ if (subscription) {
119
+ subscription.unsubscribe();
120
+ this.subscriptions.delete(channel);
121
+ }
122
+ }
123
+ /**
124
+ * Make an RPC call via Centrifugo
125
+ */
126
+ async rpc(method, data) {
127
+ if (!this.centrifuge) {
128
+ throw new Error("Not connected");
129
+ }
130
+ const result = await this.centrifuge.rpc(method, data);
131
+ return result.data;
132
+ }
133
+ /**
134
+ * Publish to a channel (fire-and-forget)
135
+ */
136
+ async publish(channel, data) {
137
+ if (!this.centrifuge) {
138
+ throw new Error("Not connected");
139
+ }
140
+ const subscription = this.subscriptions.get(channel);
141
+ if (subscription) {
142
+ await subscription.publish(data);
143
+ }
144
+ }
145
+ /**
146
+ * Get current connection state
147
+ */
148
+ getState() {
149
+ return { ...this.state };
150
+ }
151
+ /**
152
+ * Add state change listener
153
+ */
154
+ onStateChange(listener) {
155
+ this.stateListeners.add(listener);
156
+ return () => this.stateListeners.delete(listener);
157
+ }
158
+ updateState(partial) {
159
+ this.state = { ...this.state, ...partial };
160
+ this.stateListeners.forEach((listener) => listener(this.state));
161
+ }
162
+ };
163
+
164
+ // src/centrifugo/provider.tsx
165
+ import {
166
+ createContext,
167
+ useContext,
168
+ useEffect,
169
+ useState,
170
+ useCallback,
171
+ useRef
172
+ } from "react";
173
+ import { jsx } from "react/jsx-runtime";
174
+ var WebSocketContext = createContext(null);
175
+ function WebSocketProvider({
176
+ children,
177
+ url,
178
+ getToken,
179
+ autoConnect = true,
180
+ debug = false
181
+ }) {
182
+ const clientRef = useRef(null);
183
+ const [state, setState] = useState({
184
+ isConnected: false,
185
+ isConnecting: false,
186
+ error: null
187
+ });
188
+ useEffect(() => {
189
+ const client = new CMDOPWebSocketClient({
190
+ url,
191
+ getToken,
192
+ debug
193
+ });
194
+ clientRef.current = client;
195
+ const unsubscribe = client.onStateChange(setState);
196
+ if (autoConnect) {
197
+ client.connect().catch((error) => {
198
+ console.error("[CMDOP WebSocket] Auto-connect failed:", error);
199
+ });
200
+ }
201
+ return () => {
202
+ unsubscribe();
203
+ client.disconnect();
204
+ clientRef.current = null;
205
+ };
206
+ }, [url, getToken, autoConnect, debug]);
207
+ const connect = useCallback(async () => {
208
+ if (clientRef.current) {
209
+ await clientRef.current.connect();
210
+ }
211
+ }, []);
212
+ const disconnect = useCallback(() => {
213
+ if (clientRef.current) {
214
+ clientRef.current.disconnect();
215
+ }
216
+ }, []);
217
+ const value = {
218
+ client: clientRef.current,
219
+ isConnected: state.isConnected,
220
+ isConnecting: state.isConnecting,
221
+ error: state.error,
222
+ connect,
223
+ disconnect
224
+ };
225
+ return /* @__PURE__ */ jsx(WebSocketContext.Provider, { value, children });
226
+ }
227
+ function useWebSocket() {
228
+ const context = useContext(WebSocketContext);
229
+ if (!context) {
230
+ throw new Error("useWebSocket must be used within a WebSocketProvider");
231
+ }
232
+ return context;
233
+ }
234
+
235
+ // src/centrifugo/hooks.ts
236
+ import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2, useRef as useRef2 } from "react";
237
+ function useSubscription(options) {
238
+ const { channel, enabled = true, onData, onError } = options;
239
+ const { client, isConnected } = useWebSocket();
240
+ const [data, setData] = useState2(null);
241
+ const [error, setError] = useState2(null);
242
+ const [isSubscribed, setIsSubscribed] = useState2(false);
243
+ const onDataRef = useRef2(onData);
244
+ const onErrorRef = useRef2(onError);
245
+ onDataRef.current = onData;
246
+ onErrorRef.current = onError;
247
+ useEffect2(() => {
248
+ if (!client || !isConnected || !enabled || !channel) {
249
+ setIsSubscribed(false);
250
+ return;
251
+ }
252
+ const handleData = (received) => {
253
+ setData(received);
254
+ onDataRef.current?.(received);
255
+ };
256
+ const handleError = (err) => {
257
+ setError(err);
258
+ onErrorRef.current?.(err);
259
+ };
260
+ try {
261
+ const unsubscribe = client.subscribe(channel, handleData, handleError);
262
+ setIsSubscribed(true);
263
+ setError(null);
264
+ return () => {
265
+ unsubscribe();
266
+ setIsSubscribed(false);
267
+ };
268
+ } catch (err) {
269
+ const error2 = err instanceof Error ? err : new Error(String(err));
270
+ setError(error2);
271
+ onErrorRef.current?.(error2);
272
+ }
273
+ }, [client, isConnected, enabled, channel]);
274
+ return { data, error, isSubscribed };
275
+ }
276
+ function useRPC(options = {}) {
277
+ const { onError } = options;
278
+ const { client, isConnected } = useWebSocket();
279
+ const [isLoading, setIsLoading] = useState2(false);
280
+ const [error, setError] = useState2(null);
281
+ const onErrorRef = useRef2(onError);
282
+ onErrorRef.current = onError;
283
+ const call = useCallback2(
284
+ async (method, data) => {
285
+ if (!client || !isConnected) {
286
+ const err = new Error("WebSocket not connected");
287
+ setError(err);
288
+ onErrorRef.current?.(err);
289
+ throw err;
290
+ }
291
+ setIsLoading(true);
292
+ setError(null);
293
+ try {
294
+ const result = await client.rpc(method, data);
295
+ return result;
296
+ } catch (err) {
297
+ const error2 = err instanceof Error ? err : new Error(String(err));
298
+ setError(error2);
299
+ onErrorRef.current?.(error2);
300
+ throw error2;
301
+ } finally {
302
+ setIsLoading(false);
303
+ }
304
+ },
305
+ [client, isConnected]
306
+ );
307
+ const reset = useCallback2(() => {
308
+ setError(null);
309
+ }, []);
310
+ return { call, isLoading, error, reset };
311
+ }
312
+
313
+ // src/hooks/useTerminal.ts
314
+ import { useState as useState3, useCallback as useCallback3, useRef as useRef3 } from "react";
315
+ function useTerminal(options) {
316
+ const { sessionId, enabled = true, onOutput, onStatus, onError } = options;
317
+ const [output, setOutput] = useState3("");
318
+ const [status, setStatus] = useState3(null);
319
+ const [error, setError] = useState3(null);
320
+ const [isConnecting, setIsConnecting] = useState3(false);
321
+ const onOutputRef = useRef3(onOutput);
322
+ const onStatusRef = useRef3(onStatus);
323
+ const onErrorRef = useRef3(onError);
324
+ onOutputRef.current = onOutput;
325
+ onStatusRef.current = onStatus;
326
+ onErrorRef.current = onError;
327
+ const { call, isLoading: isRpcLoading } = useRPC({
328
+ onError: (err) => {
329
+ setError(err);
330
+ onErrorRef.current?.(err);
331
+ }
332
+ });
333
+ const { isSubscribed: isOutputSubscribed } = useSubscription({
334
+ channel: `terminal#${sessionId}#output`,
335
+ enabled: enabled && !!sessionId,
336
+ onData: (data) => {
337
+ const text = data.data;
338
+ setOutput((prev) => prev + text);
339
+ onOutputRef.current?.(text);
340
+ },
341
+ onError: (err) => {
342
+ setError(err);
343
+ onErrorRef.current?.(err);
344
+ }
345
+ });
346
+ const { isSubscribed: isStatusSubscribed } = useSubscription({
347
+ channel: `terminal#${sessionId}#status`,
348
+ enabled: enabled && !!sessionId,
349
+ onData: (newStatus) => {
350
+ setStatus(newStatus);
351
+ onStatusRef.current?.(newStatus);
352
+ },
353
+ onError: (err) => {
354
+ setError(err);
355
+ onErrorRef.current?.(err);
356
+ }
357
+ });
358
+ const isConnected = isOutputSubscribed && isStatusSubscribed;
359
+ const sendInput = useCallback3(
360
+ async (data) => {
361
+ if (!sessionId) return;
362
+ await call("terminal.input", {
363
+ session_id: sessionId,
364
+ data
365
+ });
366
+ },
367
+ [call, sessionId]
368
+ );
369
+ const resize = useCallback3(
370
+ async (cols, rows) => {
371
+ if (!sessionId) return;
372
+ await call("terminal.resize", {
373
+ session_id: sessionId,
374
+ cols,
375
+ rows
376
+ });
377
+ },
378
+ [call, sessionId]
379
+ );
380
+ const signal = useCallback3(
381
+ async (sig) => {
382
+ if (!sessionId) return;
383
+ const signalNum = typeof sig === "string" ? signalNameToNumber(sig) : sig;
384
+ await call("terminal.signal", {
385
+ session_id: sessionId,
386
+ signal: signalNum
387
+ });
388
+ },
389
+ [call, sessionId]
390
+ );
391
+ const clear = useCallback3(() => {
392
+ setOutput("");
393
+ }, []);
394
+ return {
395
+ isConnected,
396
+ isConnecting: isConnecting || isRpcLoading,
397
+ error,
398
+ output,
399
+ status,
400
+ sendInput,
401
+ resize,
402
+ signal,
403
+ clear
404
+ };
405
+ }
406
+ var SIGNAL_MAP = {
407
+ SIGHUP: 1,
408
+ SIGINT: 2,
409
+ SIGQUIT: 3,
410
+ SIGKILL: 9,
411
+ SIGTERM: 15,
412
+ SIGSTOP: 19,
413
+ SIGCONT: 18
414
+ };
415
+ function signalNameToNumber(name) {
416
+ const upper = name.toUpperCase();
417
+ return SIGNAL_MAP[upper] ?? parseInt(name, 10) ?? 15;
418
+ }
419
+
420
+ // src/hooks/useAgent.ts
421
+ import { useState as useState4, useCallback as useCallback4, useRef as useRef4 } from "react";
422
+ function useAgent(options) {
423
+ const { sessionId, enabled = true, onToken, onToolCall, onToolResult, onDone, onError } = options;
424
+ const [isRunning, setIsRunning] = useState4(false);
425
+ const [streamingText, setStreamingText] = useState4("");
426
+ const [result, setResult] = useState4(null);
427
+ const [toolCalls, setToolCalls] = useState4([]);
428
+ const [error, setError] = useState4(null);
429
+ const [requestId, setRequestId] = useState4(null);
430
+ const onTokenRef = useRef4(onToken);
431
+ const onToolCallRef = useRef4(onToolCall);
432
+ const onToolResultRef = useRef4(onToolResult);
433
+ const onDoneRef = useRef4(onDone);
434
+ const onErrorRef = useRef4(onError);
435
+ onTokenRef.current = onToken;
436
+ onToolCallRef.current = onToolCall;
437
+ onToolResultRef.current = onToolResult;
438
+ onDoneRef.current = onDone;
439
+ onErrorRef.current = onError;
440
+ const { call } = useRPC({
441
+ onError: (err) => {
442
+ setError(err);
443
+ setIsRunning(false);
444
+ onErrorRef.current?.(err);
445
+ }
446
+ });
447
+ useSubscription({
448
+ channel: requestId ? `agent#${requestId}#events` : "",
449
+ enabled: enabled && !!requestId && isRunning,
450
+ onData: (event) => {
451
+ switch (event.type) {
452
+ case "token": {
453
+ const tokenData = event.data;
454
+ setStreamingText((prev) => prev + tokenData.text);
455
+ onTokenRef.current?.(tokenData.text);
456
+ break;
457
+ }
458
+ case "tool_call": {
459
+ const toolCallData = event.data;
460
+ setToolCalls((prev) => [...prev, toolCallData]);
461
+ onToolCallRef.current?.(toolCallData);
462
+ break;
463
+ }
464
+ case "tool_result": {
465
+ const toolResultData = event.data;
466
+ setToolCalls((prev) => prev.filter((tc) => tc.id !== toolResultData.id));
467
+ onToolResultRef.current?.(toolResultData);
468
+ break;
469
+ }
470
+ case "done": {
471
+ const doneData = event.data;
472
+ setResult(doneData.text);
473
+ setIsRunning(false);
474
+ setRequestId(null);
475
+ onDoneRef.current?.(doneData);
476
+ break;
477
+ }
478
+ case "error": {
479
+ const errorData = event.data;
480
+ const err = new Error(errorData.message);
481
+ setError(err);
482
+ setIsRunning(false);
483
+ setRequestId(null);
484
+ onErrorRef.current?.(err);
485
+ break;
486
+ }
487
+ }
488
+ },
489
+ onError: (err) => {
490
+ setError(err);
491
+ setIsRunning(false);
492
+ onErrorRef.current?.(err);
493
+ }
494
+ });
495
+ const run = useCallback4(
496
+ async (prompt, runOptions) => {
497
+ if (!sessionId) {
498
+ throw new Error("Session ID required");
499
+ }
500
+ setStreamingText("");
501
+ setResult(null);
502
+ setToolCalls([]);
503
+ setError(null);
504
+ setIsRunning(true);
505
+ try {
506
+ const response = await call("agent.run", {
507
+ session_id: sessionId,
508
+ prompt,
509
+ mode: runOptions?.mode,
510
+ timeout_seconds: runOptions?.timeoutSeconds,
511
+ output_schema: runOptions?.outputSchema
512
+ });
513
+ setRequestId(response.request_id);
514
+ if (response.text) {
515
+ setResult(response.text);
516
+ setIsRunning(false);
517
+ return response.text;
518
+ }
519
+ return new Promise((resolve, reject) => {
520
+ const checkDone = setInterval(() => {
521
+ if (result) {
522
+ clearInterval(checkDone);
523
+ resolve(result);
524
+ }
525
+ if (error) {
526
+ clearInterval(checkDone);
527
+ reject(error);
528
+ }
529
+ }, 100);
530
+ setTimeout(() => {
531
+ clearInterval(checkDone);
532
+ if (!result && !error) {
533
+ const timeoutError = new Error("Agent timeout");
534
+ setError(timeoutError);
535
+ setIsRunning(false);
536
+ reject(timeoutError);
537
+ }
538
+ }, 3e5);
539
+ });
540
+ } catch (err) {
541
+ const error2 = err instanceof Error ? err : new Error(String(err));
542
+ setError(error2);
543
+ setIsRunning(false);
544
+ throw error2;
545
+ }
546
+ },
547
+ [call, sessionId, result, error]
548
+ );
549
+ const cancel = useCallback4(async () => {
550
+ if (!requestId) return;
551
+ try {
552
+ await call("agent.cancel", { request_id: requestId });
553
+ } finally {
554
+ setIsRunning(false);
555
+ setRequestId(null);
556
+ }
557
+ }, [call, requestId]);
558
+ const reset = useCallback4(() => {
559
+ setStreamingText("");
560
+ setResult(null);
561
+ setToolCalls([]);
562
+ setError(null);
563
+ setIsRunning(false);
564
+ setRequestId(null);
565
+ }, []);
566
+ return {
567
+ run,
568
+ isRunning,
569
+ streamingText,
570
+ result,
571
+ toolCalls,
572
+ error,
573
+ reset,
574
+ cancel
575
+ };
576
+ }
577
+
578
+ // src/index.tsx
579
+ import { createContext as createContext2, useContext as useContext2, useMemo } from "react";
580
+ import { useState as useState5, useCallback as useCallback5 } from "react";
581
+ import { machines as machines2, workspaces as workspaces2 } from "@cmdop/core";
582
+ import useSWR from "swr";
583
+ import { jsx as jsx2 } from "react/jsx-runtime";
584
+ var CMDOPContext = createContext2(null);
585
+ function CMDOPProvider({ children, apiKey, token }) {
586
+ const [currentToken, setCurrentToken] = useState5(token);
587
+ const setToken = useCallback5((newToken) => {
588
+ setCurrentToken(newToken);
589
+ machines2.setToken(newToken);
590
+ workspaces2.setToken(newToken);
591
+ }, []);
592
+ useMemo(() => {
593
+ if (currentToken) {
594
+ machines2.setToken(currentToken);
595
+ workspaces2.setToken(currentToken);
596
+ }
597
+ }, [currentToken]);
598
+ const value = useMemo(
599
+ () => ({
600
+ config: { apiKey },
601
+ isAuthenticated: !!currentToken,
602
+ setToken
603
+ }),
604
+ [apiKey, currentToken, setToken]
605
+ );
606
+ return /* @__PURE__ */ jsx2(CMDOPContext.Provider, { value, children });
607
+ }
608
+ function useCMDOP() {
609
+ const context = useContext2(CMDOPContext);
610
+ if (!context) {
611
+ throw new Error("useCMDOP must be used within a CMDOPProvider");
612
+ }
613
+ return context;
614
+ }
615
+ function useMachines(options = {}) {
616
+ const { page, pageSize, ...swrConfig } = options;
617
+ const { data, error, isLoading, isValidating, mutate } = useSWR(
618
+ ["machines", page, pageSize],
619
+ async () => {
620
+ const response = await machines2.machines_machines.machinesList({
621
+ page,
622
+ page_size: pageSize
623
+ });
624
+ return response;
625
+ },
626
+ {
627
+ revalidateOnFocus: false,
628
+ ...swrConfig
629
+ }
630
+ );
631
+ return {
632
+ machines: data?.results ?? [],
633
+ total: data?.count ?? 0,
634
+ isLoading,
635
+ isValidating,
636
+ error,
637
+ refetch: () => mutate()
638
+ };
639
+ }
640
+ function useMachine(machineId, options = {}) {
641
+ const { data, error, isLoading, mutate } = useSWR(
642
+ machineId ? ["machine", machineId] : null,
643
+ async () => {
644
+ if (!machineId) return null;
645
+ return machines2.machines_machines.machinesRetrieve(machineId);
646
+ },
647
+ {
648
+ revalidateOnFocus: false,
649
+ ...options
650
+ }
651
+ );
652
+ return {
653
+ machine: data ?? null,
654
+ isLoading,
655
+ error,
656
+ refetch: () => mutate()
657
+ };
658
+ }
659
+ function useWorkspaces(options = {}) {
660
+ const { data, error, isLoading, isValidating, mutate } = useSWR(
661
+ "workspaces",
662
+ async () => {
663
+ return workspaces2.workspaces_workspaces.workspacesList();
664
+ },
665
+ {
666
+ revalidateOnFocus: false,
667
+ ...options
668
+ }
669
+ );
670
+ return {
671
+ workspaces: data?.results ?? [],
672
+ total: data?.count ?? 0,
673
+ isLoading,
674
+ isValidating,
675
+ error,
676
+ refetch: () => mutate()
677
+ };
678
+ }
679
+ function useWorkspace(workspaceId, options = {}) {
680
+ const { data, error, isLoading, mutate } = useSWR(
681
+ workspaceId ? ["workspace", workspaceId] : null,
682
+ async () => {
683
+ if (!workspaceId) return null;
684
+ return workspaces2.workspaces_workspaces.workspacesRetrieve(workspaceId);
685
+ },
686
+ {
687
+ revalidateOnFocus: false,
688
+ ...options
689
+ }
690
+ );
691
+ return {
692
+ workspace: data ?? null,
693
+ isLoading,
694
+ error,
695
+ refetch: () => mutate()
696
+ };
697
+ }
698
+ export {
699
+ API_BASE_URL,
700
+ AuthenticationError,
701
+ CMDOPError,
702
+ CMDOPProvider,
703
+ CMDOPWebSocketClient,
704
+ CancelledError,
705
+ ConnectionError,
706
+ DEFAULT_CONFIG,
707
+ MachinesModule,
708
+ NotFoundError,
709
+ PermissionError,
710
+ ResourceExhaustedError,
711
+ SessionError,
712
+ SystemModule,
713
+ TimeoutError,
714
+ UnavailableError,
715
+ VERSION,
716
+ WebSocketProvider,
717
+ WorkspacesModule,
718
+ api,
719
+ machines,
720
+ system,
721
+ useAgent,
722
+ useCMDOP,
723
+ useMachine,
724
+ useMachines,
725
+ useRPC,
726
+ useSubscription,
727
+ useTerminal,
728
+ useWebSocket,
729
+ useWorkspace,
730
+ useWorkspaces,
731
+ workspaces
732
+ };