@trycua/cua 0.2.0

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 (91) hide show
  1. package/README.md +121 -0
  2. package/bin/cua.js +37 -0
  3. package/browser/cua_sdk-ffi.d.ts +6 -0
  4. package/browser/cua_sdk-ffi.js +3 -0
  5. package/browser/cua_sdk-ffi.ts +14 -0
  6. package/browser/cua_sdk.d.ts +2237 -0
  7. package/browser/cua_sdk.js +2607 -0
  8. package/browser/cua_sdk.ts +3644 -0
  9. package/browser/cyclops_sdk_schema-ffi.d.ts +6 -0
  10. package/browser/cyclops_sdk_schema-ffi.js +3 -0
  11. package/browser/cyclops_sdk_schema-ffi.ts +14 -0
  12. package/browser/cyclops_sdk_schema.d.ts +996 -0
  13. package/browser/cyclops_sdk_schema.js +2194 -0
  14. package/browser/cyclops_sdk_schema.ts +2934 -0
  15. package/browser/fleet_sdk-ffi.d.ts +28 -0
  16. package/browser/fleet_sdk-ffi.js +3 -0
  17. package/browser/fleet_sdk-ffi.ts +35 -0
  18. package/browser/fleet_sdk.d.ts +3056 -0
  19. package/browser/fleet_sdk.js +5088 -0
  20. package/browser/fleet_sdk.ts +6830 -0
  21. package/browser/index.d.ts +2 -0
  22. package/browser/index.js +18 -0
  23. package/browser/index.web.ts +34 -0
  24. package/browser/tsconfig.json +20 -0
  25. package/browser/wasm-bindgen/index.d.ts +2346 -0
  26. package/browser/wasm-bindgen/index.js +6642 -0
  27. package/browser/wasm-bindgen/index_bg.wasm +0 -0
  28. package/browser/wasm-bindgen/index_bg.wasm.d.ts +1154 -0
  29. package/dist/index.d.ts +118 -0
  30. package/dist/index.js +207 -0
  31. package/dist/mcp.d.ts +33 -0
  32. package/dist/mcp.js +41 -0
  33. package/dist/native/cua_sdk-ffi.d.ts +1038 -0
  34. package/dist/native/cua_sdk-ffi.js +5095 -0
  35. package/dist/native/cua_sdk.d.ts +15906 -0
  36. package/dist/native/cua_sdk.js +27061 -0
  37. package/dist/native/index.d.ts +7 -0
  38. package/dist/native/index.js +12 -0
  39. package/dist/native/node-runtime.d.ts +72 -0
  40. package/dist/native/node-runtime.js +35 -0
  41. package/dist/spaces/cursorArt.d.ts +27 -0
  42. package/dist/spaces/cursorArt.js +59 -0
  43. package/dist/spaces/errors.d.ts +81 -0
  44. package/dist/spaces/errors.js +102 -0
  45. package/dist/spaces/events.d.ts +154 -0
  46. package/dist/spaces/events.js +182 -0
  47. package/dist/spaces/groups.d.ts +130 -0
  48. package/dist/spaces/groups.js +275 -0
  49. package/dist/spaces/host.d.ts +71 -0
  50. package/dist/spaces/host.js +154 -0
  51. package/dist/spaces/index.d.ts +48 -0
  52. package/dist/spaces/index.js +44 -0
  53. package/dist/spaces/pip.d.ts +128 -0
  54. package/dist/spaces/pip.js +250 -0
  55. package/dist/spaces/presence.d.ts +187 -0
  56. package/dist/spaces/presence.js +449 -0
  57. package/dist/spaces/presenceTypes.check.d.ts +12 -0
  58. package/dist/spaces/presenceTypes.check.js +1 -0
  59. package/dist/spaces/presenceTypes.d.ts +47 -0
  60. package/dist/spaces/presenceTypes.js +7 -0
  61. package/dist/spaces/routines.d.ts +158 -0
  62. package/dist/spaces/routines.js +339 -0
  63. package/dist/spaces/thread.d.ts +147 -0
  64. package/dist/spaces/thread.js +285 -0
  65. package/dist/spaces/transport/http.d.ts +51 -0
  66. package/dist/spaces/transport/http.js +113 -0
  67. package/dist/spaces/transport/index.d.ts +19 -0
  68. package/dist/spaces/transport/index.js +19 -0
  69. package/dist/spaces/transport/session.d.ts +120 -0
  70. package/dist/spaces/transport/session.js +188 -0
  71. package/dist/spaces/transport/tauri.d.ts +50 -0
  72. package/dist/spaces/transport/tauri.js +90 -0
  73. package/dist/spaces/transport/types.d.ts +123 -0
  74. package/dist/spaces/transport/types.js +59 -0
  75. package/dist/teleport/controller.d.ts +34 -0
  76. package/dist/teleport/controller.js +107 -0
  77. package/dist/teleport/drop.d.ts +62 -0
  78. package/dist/teleport/drop.js +138 -0
  79. package/dist/teleport/dropZone.d.ts +59 -0
  80. package/dist/teleport/dropZone.js +166 -0
  81. package/dist/teleport/element.d.ts +28 -0
  82. package/dist/teleport/element.js +340 -0
  83. package/dist/teleport/index.d.ts +32 -0
  84. package/dist/teleport/index.js +32 -0
  85. package/dist/teleport/install.d.ts +31 -0
  86. package/dist/teleport/install.js +65 -0
  87. package/dist/teleport/model.d.ts +245 -0
  88. package/dist/teleport/model.js +334 -0
  89. package/dist/teleport/windowDrag.d.ts +86 -0
  90. package/dist/teleport/windowDrag.js +71 -0
  91. package/package.json +93 -0
@@ -0,0 +1,120 @@
1
+ /**
2
+ * `McpSession` — the MCP vocabulary, over any {@link Transport}.
3
+ *
4
+ * This is the piece that makes the seam worth having: it is the same code in
5
+ * Node, in a Tauri webview and in a browser, and the only thing that differs
6
+ * between them is which transport it was constructed with.
7
+ *
8
+ * It is deliberately small. It owns the handshake, id allocation, and the
9
+ * unwrapping of the `content` / `isError` envelope — and nothing else. The
10
+ * server on the other side is the Rust Spaces MCP server (`cua daemon mcp`,
11
+ * the daemon's `/mcp`, or the Tauri app's bridge); its tools are the Spaces
12
+ * contract (`libs/cua/spaces-contract/manifest.json`). Node code that can load
13
+ * the native binding uses `@trycua/cua/spaces` (typed) instead.
14
+ */
15
+ import { type Json, type SendOptions, type Transport } from "./types.js";
16
+ /** The MCP revision offered. The Rust server accepts 2025-06-18, 2025-03-26
17
+ * and 2024-11-05, and answers with the one it picked. */
18
+ export declare const PROTOCOL_VERSION = "2025-06-18";
19
+ export interface ClientInfo {
20
+ name: string;
21
+ version: string;
22
+ }
23
+ export interface ServerInfo {
24
+ name: string;
25
+ version: string;
26
+ }
27
+ /** One part of a tool result. `call_tool` passes through `image`,
28
+ * `audio` and `resource` parts from an in-space tool unchanged, so this is not
29
+ * only ever text. */
30
+ export interface ContentPart {
31
+ type: string;
32
+ text?: string;
33
+ [key: string]: Json | undefined;
34
+ }
35
+ export interface ToolResult {
36
+ content: ContentPart[];
37
+ isError: boolean;
38
+ /** `structuredContent`, when the tool returned one (tool errors carry
39
+ * `{error: {kind, message}}` with a stable `kind`). */
40
+ structured?: Json;
41
+ }
42
+ export interface ToolDescriptor {
43
+ name: string;
44
+ description?: string;
45
+ inputSchema?: Json;
46
+ }
47
+ /**
48
+ * A tool that ran and failed.
49
+ *
50
+ * This is a separate type from {@link TransportError} on purpose. The server
51
+ * reports a failing tool as a *successful* JSON-RPC response carrying
52
+ * `isError: true` — the only JSON-RPC `error` objects it ever sends are
53
+ * `-32601` for an unknown method or tool. Collapsing the two would make
54
+ * "the sandbox refused this command" indistinguishable from "the control
55
+ * plane is not running", and those need different handling by every caller.
56
+ */
57
+ export declare class ToolError extends Error {
58
+ readonly tag = "ToolError";
59
+ readonly tool: string;
60
+ readonly content: ContentPart[];
61
+ /** The server's stable error kind (`capability_missing`,
62
+ * `teleport_refused`, `not_found`, ...), when it sent one. */
63
+ readonly kind: string | undefined;
64
+ constructor(tool: string, content: ContentPart[], structured?: Json);
65
+ }
66
+ /** Concatenate the text parts, which is what almost every caller wants. */
67
+ export declare function textOf(content: ContentPart[]): string;
68
+ export interface SessionOptions {
69
+ clientInfo?: ClientInfo;
70
+ }
71
+ export declare class McpSession {
72
+ readonly transport: Transport;
73
+ private nextId;
74
+ private handshake;
75
+ private readonly clientInfo;
76
+ constructor(transport: Transport, options?: SessionOptions);
77
+ private allocateId;
78
+ private unwrap;
79
+ /**
80
+ * Perform the MCP handshake, at most once per session.
81
+ *
82
+ * The server does not require this — it builds its dispatch table before the
83
+ * read loop starts, so `tools/call` works cold. We do it anyway, and we
84
+ * memoize it: it is what makes this session usable against a *conforming*
85
+ * MCP server rather than only against this one, and the cost is a single
86
+ * round trip.
87
+ *
88
+ * The result is cached as a promise, not a value, so concurrent first calls
89
+ * share one handshake instead of racing two.
90
+ */
91
+ initialize(options?: SendOptions): Promise<ServerInfo | undefined>;
92
+ private performHandshake;
93
+ /** Sends a request after the handshake; re-initializes once when the
94
+ * server says the session expired (HTTP 404 from a restarted daemon). */
95
+ private request;
96
+ /** Every tool the server serves, with its input schema. */
97
+ listTools(options?: SendOptions): Promise<ToolDescriptor[]>;
98
+ /**
99
+ * Call a tool and return its envelope, including a failure.
100
+ *
101
+ * Use this when a caller wants to inspect `isError` itself — for instance a
102
+ * probe that treats "not available" as an answer rather than an exception.
103
+ * Most callers want {@link call}.
104
+ */
105
+ callRaw(name: string, args?: Record<string, Json>, options?: SendOptions): Promise<ToolResult>;
106
+ /** Call a tool, throwing {@link ToolError} if it failed. */
107
+ call(name: string, args?: Record<string, Json>, options?: SendOptions): Promise<ContentPart[]>;
108
+ /** Call a tool and return its text parts joined — the common case, since
109
+ * most of these tools answer with one text part. */
110
+ callText(name: string, args?: Record<string, Json>, options?: SendOptions): Promise<string>;
111
+ /**
112
+ * Call a tool whose text part is JSON, and parse it.
113
+ *
114
+ * Worth its own method because the parse failure needs to say which tool
115
+ * produced the unparsable text — without that, a server-side format change
116
+ * surfaces as a bare `SyntaxError` with no attribution.
117
+ */
118
+ callJson<T = Json>(name: string, args?: Record<string, Json>, options?: SendOptions): Promise<T>;
119
+ close(): Promise<void>;
120
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * `McpSession` — the MCP vocabulary, over any {@link Transport}.
3
+ *
4
+ * This is the piece that makes the seam worth having: it is the same code in
5
+ * Node, in a Tauri webview and in a browser, and the only thing that differs
6
+ * between them is which transport it was constructed with.
7
+ *
8
+ * It is deliberately small. It owns the handshake, id allocation, and the
9
+ * unwrapping of the `content` / `isError` envelope — and nothing else. The
10
+ * server on the other side is the Rust Spaces MCP server (`cua daemon mcp`,
11
+ * the daemon's `/mcp`, or the Tauri app's bridge); its tools are the Spaces
12
+ * contract (`libs/cua/spaces-contract/manifest.json`). Node code that can load
13
+ * the native binding uses `@trycua/cua/spaces` (typed) instead.
14
+ */
15
+ import { TransportError, } from "./types.js";
16
+ /** The MCP revision offered. The Rust server accepts 2025-06-18, 2025-03-26
17
+ * and 2024-11-05, and answers with the one it picked. */
18
+ export const PROTOCOL_VERSION = "2025-06-18";
19
+ /**
20
+ * A tool that ran and failed.
21
+ *
22
+ * This is a separate type from {@link TransportError} on purpose. The server
23
+ * reports a failing tool as a *successful* JSON-RPC response carrying
24
+ * `isError: true` — the only JSON-RPC `error` objects it ever sends are
25
+ * `-32601` for an unknown method or tool. Collapsing the two would make
26
+ * "the sandbox refused this command" indistinguishable from "the control
27
+ * plane is not running", and those need different handling by every caller.
28
+ */
29
+ export class ToolError extends Error {
30
+ tag = "ToolError";
31
+ tool;
32
+ content;
33
+ /** The server's stable error kind (`capability_missing`,
34
+ * `teleport_refused`, `not_found`, ...), when it sent one. */
35
+ kind;
36
+ constructor(tool, content, structured) {
37
+ super(`${tool}: ${textOf(content) || "failed"}`);
38
+ this.name = "ToolError";
39
+ this.tool = tool;
40
+ this.content = content;
41
+ const error = structured?.error;
42
+ this.kind = typeof error?.kind === "string" ? error.kind : undefined;
43
+ }
44
+ }
45
+ /** Concatenate the text parts, which is what almost every caller wants. */
46
+ export function textOf(content) {
47
+ return content
48
+ .filter((part) => part.type === "text" && typeof part.text === "string")
49
+ .map((part) => part.text)
50
+ .join("\n");
51
+ }
52
+ export class McpSession {
53
+ transport;
54
+ nextId = 1;
55
+ handshake;
56
+ clientInfo;
57
+ constructor(transport, options = {}) {
58
+ this.transport = transport;
59
+ this.clientInfo = options.clientInfo ?? { name: "@trycua/cua", version: "0.2.0" };
60
+ }
61
+ allocateId() {
62
+ return this.nextId++;
63
+ }
64
+ unwrap(response, method) {
65
+ if (response === null) {
66
+ throw new TransportError(`${method} expected a reply and got none`);
67
+ }
68
+ if (response.error) {
69
+ throw new TransportError(`${method}: ${response.error.message}`, response.error.code);
70
+ }
71
+ return response.result ?? null;
72
+ }
73
+ /**
74
+ * Perform the MCP handshake, at most once per session.
75
+ *
76
+ * The server does not require this — it builds its dispatch table before the
77
+ * read loop starts, so `tools/call` works cold. We do it anyway, and we
78
+ * memoize it: it is what makes this session usable against a *conforming*
79
+ * MCP server rather than only against this one, and the cost is a single
80
+ * round trip.
81
+ *
82
+ * The result is cached as a promise, not a value, so concurrent first calls
83
+ * share one handshake instead of racing two.
84
+ */
85
+ async initialize(options) {
86
+ if (!this.handshake) {
87
+ this.handshake = this.performHandshake(options).catch((error) => {
88
+ // A failed handshake must not poison the session forever; a later call
89
+ // should be free to try again against a restarted server.
90
+ this.handshake = undefined;
91
+ throw error;
92
+ });
93
+ }
94
+ return this.handshake;
95
+ }
96
+ async performHandshake(options) {
97
+ const result = this.unwrap(await this.transport.send({
98
+ jsonrpc: "2.0",
99
+ id: this.allocateId(),
100
+ method: "initialize",
101
+ params: {
102
+ protocolVersion: PROTOCOL_VERSION,
103
+ capabilities: {},
104
+ clientInfo: { ...this.clientInfo },
105
+ },
106
+ }, options), "initialize");
107
+ await this.transport.send({ jsonrpc: "2.0", method: "notifications/initialized" }, options);
108
+ const info = result?.serverInfo;
109
+ return info;
110
+ }
111
+ /** Sends a request after the handshake; re-initializes once when the
112
+ * server says the session expired (HTTP 404 from a restarted daemon). */
113
+ async request(method, params, options) {
114
+ for (let attempt = 0;; attempt++) {
115
+ await this.initialize(options);
116
+ try {
117
+ const message = params === undefined
118
+ ? { jsonrpc: "2.0", id: this.allocateId(), method }
119
+ : { jsonrpc: "2.0", id: this.allocateId(), method, params };
120
+ return this.unwrap(await this.transport.send(message, options), method);
121
+ }
122
+ catch (error) {
123
+ if (attempt === 0 && error instanceof TransportError && error.code === 404) {
124
+ this.handshake = undefined;
125
+ continue;
126
+ }
127
+ throw error;
128
+ }
129
+ }
130
+ }
131
+ /** Every tool the server serves, with its input schema. */
132
+ async listTools(options) {
133
+ const result = await this.request("tools/list", undefined, options);
134
+ const tools = result?.tools;
135
+ return Array.isArray(tools) ? tools : [];
136
+ }
137
+ /**
138
+ * Call a tool and return its envelope, including a failure.
139
+ *
140
+ * Use this when a caller wants to inspect `isError` itself — for instance a
141
+ * probe that treats "not available" as an answer rather than an exception.
142
+ * Most callers want {@link call}.
143
+ */
144
+ async callRaw(name, args = {}, options) {
145
+ const result = (await this.request("tools/call", { name, arguments: args }, options));
146
+ const content = Array.isArray(result?.content) ? result.content : [];
147
+ // `isError` is read as a field and never inferred from the shape of the
148
+ // content: a missing flag means the server did not say, and reading
149
+ // silence as success is how a failure becomes a wrong answer instead of an
150
+ // error. This server always sends it explicitly, including `false`.
151
+ const out = { content, isError: result?.isError === true };
152
+ if (result?.structuredContent !== undefined)
153
+ out.structured = result.structuredContent;
154
+ return out;
155
+ }
156
+ /** Call a tool, throwing {@link ToolError} if it failed. */
157
+ async call(name, args = {}, options) {
158
+ const result = await this.callRaw(name, args, options);
159
+ if (result.isError)
160
+ throw new ToolError(name, result.content, result.structured);
161
+ return result.content;
162
+ }
163
+ /** Call a tool and return its text parts joined — the common case, since
164
+ * most of these tools answer with one text part. */
165
+ async callText(name, args = {}, options) {
166
+ return textOf(await this.call(name, args, options));
167
+ }
168
+ /**
169
+ * Call a tool whose text part is JSON, and parse it.
170
+ *
171
+ * Worth its own method because the parse failure needs to say which tool
172
+ * produced the unparsable text — without that, a server-side format change
173
+ * surfaces as a bare `SyntaxError` with no attribution.
174
+ */
175
+ async callJson(name, args = {}, options) {
176
+ const text = await this.callText(name, args, options);
177
+ try {
178
+ return JSON.parse(text);
179
+ }
180
+ catch {
181
+ throw new TransportError(`${name} did not return JSON: ${text.slice(0, 200)}`);
182
+ }
183
+ }
184
+ async close() {
185
+ this.handshake = undefined;
186
+ await this.transport.close();
187
+ }
188
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * `TauriTransport` — the conformer that makes this SDK usable from a webview.
3
+ *
4
+ * A Tauri frontend cannot load the package's N-API binding, so the root
5
+ * export cannot run there. What it *can* do is `invoke()` a Rust command
6
+ * that hands the JSON-RPC message to the Rust Spaces MCP server (in process
7
+ * via `cua_spaces::mcp::McpServer::handle`, or the daemon), and this class
8
+ * is the lines that carry a message across that call.
9
+ *
10
+ * Everything above it — `McpSession`, the handshake, the tool vocabulary, the
11
+ * `isError` unwrapping — is the same code a Node consumer runs. That is the
12
+ * claim the seam exists to make true.
13
+ *
14
+ * # Why `invoke` is injected rather than imported
15
+ *
16
+ * This package does not depend on `@tauri-apps/api`, and should not: a Node
17
+ * or browser consumer installing the Spaces SDK should not acquire a desktop
18
+ * framework. The host passes its own `invoke`, which also makes the transport
19
+ * trivially testable with a function that returns canned replies — the
20
+ * conformance test does exactly that.
21
+ */
22
+ import { type RpcOutgoing, type RpcResponse, type SendOptions, type Transport } from "./types.js";
23
+ /** The shape of Tauri's `invoke`, narrowed to what this needs. */
24
+ export type InvokeFn = <T>(command: string, args?: Record<string, unknown>) => Promise<T>;
25
+ export interface TauriOptions {
26
+ /** Usually `(await import("@tauri-apps/api/core")).invoke`. */
27
+ invoke: InvokeFn;
28
+ /** The Rust command name (default `spaces_mcp_request`). */
29
+ requestCommand?: string;
30
+ /** The Rust command that ends the session (default `spaces_mcp_shutdown`). */
31
+ shutdownCommand?: string;
32
+ }
33
+ export declare const DEFAULT_REQUEST_COMMAND = "spaces_mcp_request";
34
+ export declare const DEFAULT_SHUTDOWN_COMMAND = "spaces_mcp_shutdown";
35
+ export declare class TauriTransport implements Transport {
36
+ readonly kind = "tauri";
37
+ private readonly invoke;
38
+ private readonly requestCommand;
39
+ private readonly shutdownCommand;
40
+ constructor(options: TauriOptions);
41
+ send(message: RpcOutgoing, options?: SendOptions): Promise<RpcResponse | null>;
42
+ /**
43
+ * Ask the bridge to close the server's stdin.
44
+ *
45
+ * A failure here is swallowed: the transport is being torn down, the app may
46
+ * already be quitting, and turning "could not reach the bridge while closing"
47
+ * into an exception makes shutdown paths fragile for no gain.
48
+ */
49
+ close(): Promise<void>;
50
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * `TauriTransport` — the conformer that makes this SDK usable from a webview.
3
+ *
4
+ * A Tauri frontend cannot load the package's N-API binding, so the root
5
+ * export cannot run there. What it *can* do is `invoke()` a Rust command
6
+ * that hands the JSON-RPC message to the Rust Spaces MCP server (in process
7
+ * via `cua_spaces::mcp::McpServer::handle`, or the daemon), and this class
8
+ * is the lines that carry a message across that call.
9
+ *
10
+ * Everything above it — `McpSession`, the handshake, the tool vocabulary, the
11
+ * `isError` unwrapping — is the same code a Node consumer runs. That is the
12
+ * claim the seam exists to make true.
13
+ *
14
+ * # Why `invoke` is injected rather than imported
15
+ *
16
+ * This package does not depend on `@tauri-apps/api`, and should not: a Node
17
+ * or browser consumer installing the Spaces SDK should not acquire a desktop
18
+ * framework. The host passes its own `invoke`, which also makes the transport
19
+ * trivially testable with a function that returns canned replies — the
20
+ * conformance test does exactly that.
21
+ */
22
+ import { TransportError, } from "./types.js";
23
+ export const DEFAULT_REQUEST_COMMAND = "spaces_mcp_request";
24
+ export const DEFAULT_SHUTDOWN_COMMAND = "spaces_mcp_shutdown";
25
+ export class TauriTransport {
26
+ kind = "tauri";
27
+ invoke;
28
+ requestCommand;
29
+ shutdownCommand;
30
+ constructor(options) {
31
+ this.invoke = options.invoke;
32
+ this.requestCommand = options.requestCommand ?? DEFAULT_REQUEST_COMMAND;
33
+ this.shutdownCommand = options.shutdownCommand ?? DEFAULT_SHUTDOWN_COMMAND;
34
+ }
35
+ async send(message, options = {}) {
36
+ if (options.signal?.aborted) {
37
+ throw new TransportError(`${message.method} was aborted`);
38
+ }
39
+ // The Rust side owns the wait, including the timeout, because it owns the
40
+ // pending-reply map: a deadline enforced only here would leave the bridge
41
+ // holding a waiter for a reply nobody wants any more.
42
+ const call = this.invoke(this.requestCommand, {
43
+ message: message,
44
+ timeoutSecs: options.timeoutSeconds ?? null,
45
+ });
46
+ const value = options.signal ? await race(call, options.signal, message.method) : await call;
47
+ // A notification gets no reply, and the bridge answers `null` for it
48
+ // rather than inventing one.
49
+ if (value === null || value === undefined)
50
+ return null;
51
+ if (typeof value !== "object" || Array.isArray(value)) {
52
+ throw new TransportError(`${message.method}: the bridge returned a non-object reply`);
53
+ }
54
+ return value;
55
+ }
56
+ /**
57
+ * Ask the bridge to close the server's stdin.
58
+ *
59
+ * A failure here is swallowed: the transport is being torn down, the app may
60
+ * already be quitting, and turning "could not reach the bridge while closing"
61
+ * into an exception makes shutdown paths fragile for no gain.
62
+ */
63
+ async close() {
64
+ try {
65
+ await this.invoke(this.shutdownCommand);
66
+ }
67
+ catch {
68
+ /* the bridge is gone, which is the state we wanted */
69
+ }
70
+ }
71
+ }
72
+ /**
73
+ * Abort support for a call we cannot actually cancel.
74
+ *
75
+ * The server has no `notifications/cancelled` handling and is strictly
76
+ * sequential, so aborting abandons the reply — the work still runs to
77
+ * completion. This is honest about that rather than implying a cancel: the
78
+ * promise rejects, the bridge's waiter is left to its own timeout.
79
+ */
80
+ async function race(promise, signal, method) {
81
+ return await Promise.race([
82
+ promise,
83
+ new Promise((_resolve, reject) => {
84
+ const onAbort = () => reject(new TransportError(`${method} was abandoned`));
85
+ if (signal.aborted)
86
+ return onAbort();
87
+ signal.addEventListener("abort", onAbort, { once: true });
88
+ }),
89
+ ]);
90
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * The transport seam.
3
+ *
4
+ * # Why a seam at all
5
+ *
6
+ * The Spaces control plane is the Rust MCP server (`cua daemon mcp` over
7
+ * stdio, the daemon's `/mcp` over HTTP). Three of the four places this SDK
8
+ * has to run cannot spawn a subprocess:
9
+ *
10
+ * | host | can spawn a subprocess? | can load the N-API binding? |
11
+ * |---|---|---|
12
+ * | Node | yes | yes |
13
+ * | Tauri webview (`apps/cua-spaces`) | no | no |
14
+ * | browser | no | no |
15
+ * | React Native | no | yes, via UBRN |
16
+ *
17
+ * So a TypeScript SDK that owns its own stdio transport can only ever run in
18
+ * Node, and "the TypeScript SDK" would not be the thing our own app uses. The
19
+ * transport is what has to be pluggable for the same API to be true
20
+ * everywhere; everything above it — the handshake, the tool vocabulary, the
21
+ * error shape — is identical whatever carries the bytes.
22
+ *
23
+ * # Where the seam falls, and why it is not at the byte level
24
+ *
25
+ * A `Transport` exchanges **whole JSON-RPC messages**, not bytes. That is the
26
+ * load-bearing choice.
27
+ *
28
+ * Line framing is protocol. It is the bug the architecture document calls §1
29
+ * — a framer that dropped every byte after the first newline in a chunk — and
30
+ * it is invisible until it is catastrophic. If the seam were a byte stream,
31
+ * every host would re-implement that framer, and the whole argument for one
32
+ * Rust core would be conceded at exactly the layer that has already been got
33
+ * wrong once.
34
+ *
35
+ * At the message level the framer lives wherever the server lives: in the
36
+ * Rust `cua_spaces::mcp::stdio` framer, the daemon's HTTP endpoint, or the
37
+ * Tauri app's Rust side. None of them is the renderer's problem.
38
+ *
39
+ * # Nothing in this file imports the native binding
40
+ *
41
+ * This module and everything under `src/transport/` is dependency-free,
42
+ * platform-free TypeScript. It is exported from `@trycua/cua/spaces/transport`
43
+ * rather than the package root precisely so a webview bundle can import it
44
+ * without dragging in `@ubjs/node`, which would fail to resolve and take the
45
+ * whole bundle with it.
46
+ */
47
+ /** A JSON value, as it crosses the wire. */
48
+ export type Json = null | boolean | number | string | Json[] | {
49
+ [key: string]: Json;
50
+ };
51
+ /** JSON-RPC 2.0 ids: this SDK only ever sends numbers, but a reply echoes
52
+ * whatever it was given, and a conforming peer may use a string. */
53
+ export type RpcId = number | string;
54
+ export interface RpcRequest {
55
+ jsonrpc: "2.0";
56
+ id: RpcId;
57
+ method: string;
58
+ params?: Json;
59
+ }
60
+ /** A message with no `id`. It gets no reply, and a transport must resolve
61
+ * `null` for it rather than inventing one. */
62
+ export interface RpcNotification {
63
+ jsonrpc: "2.0";
64
+ method: string;
65
+ params?: Json;
66
+ }
67
+ export interface RpcError {
68
+ code: number;
69
+ message: string;
70
+ data?: Json;
71
+ }
72
+ export interface RpcResponse {
73
+ jsonrpc: "2.0";
74
+ id: RpcId;
75
+ result?: Json;
76
+ error?: RpcError;
77
+ }
78
+ export type RpcOutgoing = RpcRequest | RpcNotification;
79
+ export interface SendOptions {
80
+ /**
81
+ * Seconds to wait for a reply.
82
+ *
83
+ * The default is deliberately long. `teleport_app` and provisioning run with
84
+ * `timeout=900` server-side, and the agent-CLI registration this server
85
+ * ships with sets `requestTimeoutMs: 900000`. A transport that imposed a
86
+ * short deadline would silently break the app's slowest and most valuable
87
+ * operations, so a conformer must not shorten this on its own initiative.
88
+ */
89
+ timeoutSeconds?: number;
90
+ /** Aborts the wait. A transport should still expect the server to finish the
91
+ * work: this server has no `notifications/cancelled` handling, so aborting
92
+ * abandons the reply rather than stopping the call. */
93
+ signal?: AbortSignal;
94
+ }
95
+ /**
96
+ * The one interface a host has to implement.
97
+ *
98
+ * Implementations must:
99
+ * - resolve the reply whose `id` matches the request's, and not some other
100
+ * reply that happened to arrive first;
101
+ * - resolve `null` for a notification;
102
+ * - reject every in-flight call if the peer dies, rather than leaving callers
103
+ * hanging on a reply that is never coming;
104
+ * - be safe to call again after `close()`, either by restarting or by
105
+ * rejecting clearly.
106
+ */
107
+ export interface Transport {
108
+ /** A short name for diagnostics — `"stdio"`, `"tauri"`. */
109
+ readonly kind: string;
110
+ /** Send one message; resolve its reply, or `null` for a notification. */
111
+ send(message: RpcOutgoing, options?: SendOptions): Promise<RpcResponse | null>;
112
+ /** Release the peer. Idempotent. */
113
+ close(): Promise<void>;
114
+ }
115
+ /** Thrown for anything that goes wrong beneath the tool call: a dead peer, a
116
+ * timeout, a JSON-RPC `error` object. A failing *tool* is not this — see
117
+ * `ToolError`, because this server reports tool failure as a successful
118
+ * response carrying `isError: true`. */
119
+ export declare class TransportError extends Error {
120
+ readonly tag = "TransportError";
121
+ readonly code: number | undefined;
122
+ constructor(message: string, code?: number);
123
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The transport seam.
3
+ *
4
+ * # Why a seam at all
5
+ *
6
+ * The Spaces control plane is the Rust MCP server (`cua daemon mcp` over
7
+ * stdio, the daemon's `/mcp` over HTTP). Three of the four places this SDK
8
+ * has to run cannot spawn a subprocess:
9
+ *
10
+ * | host | can spawn a subprocess? | can load the N-API binding? |
11
+ * |---|---|---|
12
+ * | Node | yes | yes |
13
+ * | Tauri webview (`apps/cua-spaces`) | no | no |
14
+ * | browser | no | no |
15
+ * | React Native | no | yes, via UBRN |
16
+ *
17
+ * So a TypeScript SDK that owns its own stdio transport can only ever run in
18
+ * Node, and "the TypeScript SDK" would not be the thing our own app uses. The
19
+ * transport is what has to be pluggable for the same API to be true
20
+ * everywhere; everything above it — the handshake, the tool vocabulary, the
21
+ * error shape — is identical whatever carries the bytes.
22
+ *
23
+ * # Where the seam falls, and why it is not at the byte level
24
+ *
25
+ * A `Transport` exchanges **whole JSON-RPC messages**, not bytes. That is the
26
+ * load-bearing choice.
27
+ *
28
+ * Line framing is protocol. It is the bug the architecture document calls §1
29
+ * — a framer that dropped every byte after the first newline in a chunk — and
30
+ * it is invisible until it is catastrophic. If the seam were a byte stream,
31
+ * every host would re-implement that framer, and the whole argument for one
32
+ * Rust core would be conceded at exactly the layer that has already been got
33
+ * wrong once.
34
+ *
35
+ * At the message level the framer lives wherever the server lives: in the
36
+ * Rust `cua_spaces::mcp::stdio` framer, the daemon's HTTP endpoint, or the
37
+ * Tauri app's Rust side. None of them is the renderer's problem.
38
+ *
39
+ * # Nothing in this file imports the native binding
40
+ *
41
+ * This module and everything under `src/transport/` is dependency-free,
42
+ * platform-free TypeScript. It is exported from `@trycua/cua/spaces/transport`
43
+ * rather than the package root precisely so a webview bundle can import it
44
+ * without dragging in `@ubjs/node`, which would fail to resolve and take the
45
+ * whole bundle with it.
46
+ */
47
+ /** Thrown for anything that goes wrong beneath the tool call: a dead peer, a
48
+ * timeout, a JSON-RPC `error` object. A failing *tool* is not this — see
49
+ * `ToolError`, because this server reports tool failure as a successful
50
+ * response carrying `isError: true`. */
51
+ export class TransportError extends Error {
52
+ tag = "TransportError";
53
+ code;
54
+ constructor(message, code) {
55
+ super(message);
56
+ this.name = "TransportError";
57
+ this.code = code;
58
+ }
59
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * {@link TeleportPickerController}: the headless state machine bound to a
3
+ * {@link TeleportHost}. Frameworks subscribe to it (React:
4
+ * `useSyncExternalStore(c.subscribe, () => c.state)`; the web component
5
+ * re-renders on each change).
6
+ */
7
+ import { type CatalogEntry, type PickerEvent, type PickerState, type TeleportHost } from "./model.js";
8
+ export interface ControllerOptions {
9
+ spaceName: string;
10
+ /** Jump straight to the options for this app (a drop or window drag). */
11
+ preselect?: {
12
+ entry: CatalogEntry;
13
+ files?: string[];
14
+ };
15
+ }
16
+ export declare class TeleportPickerController {
17
+ #private;
18
+ readonly host: TeleportHost;
19
+ constructor(host: TeleportHost, options: ControllerOptions);
20
+ get state(): PickerState;
21
+ /** Subscribe to changes; returns the unsubscribe. */
22
+ subscribe: (listener: () => void) => (() => void);
23
+ dispatch: (event: PickerEvent) => void;
24
+ /** Loads (or reloads) the catalog. */
25
+ load(): Promise<void>;
26
+ /** Opens the options for the selected (or given) app. */
27
+ choose(id?: string): void;
28
+ /** Opens the native chooser and adds what the user picked. */
29
+ chooseFiles(): Promise<void>;
30
+ /** Builds the plan (sizes, installs, consent items). */
31
+ plan(): Promise<void>;
32
+ /** Runs the approved plan with the user's consent. */
33
+ confirm(): Promise<void>;
34
+ }