@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,147 @@
1
+ /**
2
+ * Agent threads: an agent CLI running a task in a Space, with an explicit
3
+ * isolation choice, a closed status ladder, and derived events.
4
+ *
5
+ * The run itself is the SDK's (`Space.agentStart` / `agentStatus` /
6
+ * `agentMessage` / `agentStop`, the same implementation as the Spaces MCP
7
+ * `agent_*` tools): a detached, tagged cua-spacesd process whose status is
8
+ * read from process liveness and its recorded exit code. This module adds
9
+ * what an app needs on top: a placement you have to choose, per-Space turn
10
+ * serialization for shared Spaces, and `events()`.
11
+ */
12
+ import type { SpaceLike, SpacesLike } from "../native/index.js";
13
+ import { type AgentStatus, type ThreadEvent, type ThreadStatus } from "./events.js";
14
+ /** Agent CLIs the Spaces runtime can start (the contract's `AGENT_IDS`). */
15
+ export type AgentId = "claude-code" | "gemini-cli" | "google-antigravity" | "goose" | "hermes" | "openai-codex" | "openclaw" | "opencode" | "pi";
16
+ export declare const AGENT_IDS: readonly AgentId[];
17
+ /** Statuses after which `events()` stops polling. */
18
+ export declare const SETTLED: ReadonlySet<AgentStatus>;
19
+ /**
20
+ * Where a thread's agent runs. There is no default; choosing is the point.
21
+ */
22
+ export type ThreadPlacement = {
23
+ type: "dedicated";
24
+ /** Image of the Space this thread creates (default: the canonical Linux image). */
25
+ image?: string;
26
+ /** Where: `local` or `cloud` (default: the user's default location). */
27
+ on?: string;
28
+ /** `auto` (default), `container` or `vm`. */
29
+ kind?: string;
30
+ /** `auto` (default) or an engine the location offers for the kind. */
31
+ runtime?: string;
32
+ /** Delete the Space when the thread closes. Default true. */
33
+ deleteOnClose?: boolean;
34
+ } | {
35
+ type: "shared";
36
+ /** The Space (id or handle) to place this thread on. */
37
+ space: string | SpaceLike;
38
+ /**
39
+ * Required, and required to be literally `true`: this thread shares a
40
+ * filesystem, a browser profile, cookies and every app login with every
41
+ * other thread on the Space; nothing inside a Space is a security
42
+ * boundary.
43
+ */
44
+ acknowledgeNoIsolation: true;
45
+ };
46
+ /** Throws unless the caller genuinely chose a placement. */
47
+ export declare function validatePlacement(placement: ThreadPlacement | undefined): ThreadPlacement;
48
+ /** One delivered (or refused) message. */
49
+ export interface Turn {
50
+ id: string;
51
+ text: string;
52
+ /** `delivered` means the process started, not that the work is done. */
53
+ state: "delivered" | "refused";
54
+ /** The turn this one waited behind on a shared Space, if any. */
55
+ queuedBehind?: string;
56
+ reason: string;
57
+ }
58
+ /**
59
+ * Serializes turns per Space, shared by every thread on that Space, so two
60
+ * bots on one Space take the Space in turn instead of fighting over it.
61
+ */
62
+ export declare class SpaceTurnLock {
63
+ private tail;
64
+ private activeTurn;
65
+ /** The turn currently holding the Space, or null. */
66
+ get active(): string | null;
67
+ /** Queues `turnId` now (call order, not scheduling order). */
68
+ enqueue(turnId: string): {
69
+ waitingBehind: string | null;
70
+ acquired: Promise<() => void>;
71
+ };
72
+ private take;
73
+ }
74
+ /** One lock per Space id, so two handles to the same Space share it. */
75
+ export declare function lockFor(spaceId: string): SpaceTurnLock;
76
+ export interface StartThreadOptions {
77
+ agent: AgentId;
78
+ /** The first task. There is no empty thread. */
79
+ prompt: string;
80
+ placement: ThreadPlacement;
81
+ /** Your own label, echoed on the thread. */
82
+ label?: string;
83
+ /** Open a terminal on the Space desktop tailing the run (default false). */
84
+ show?: boolean;
85
+ }
86
+ /** Starts an agent thread. `placement` is required and has no default. */
87
+ export declare function startThread(spaces: SpacesLike, options: StartThreadOptions): Promise<Thread>;
88
+ /** Adopts a run that already exists (started by another process or the MCP). */
89
+ export declare function adoptThread(spaces: SpacesLike, spaceId: string, runId: string): Promise<Thread>;
90
+ export interface EventOptions {
91
+ /** Hard bound on status polls (default 600). */
92
+ maxPolls?: number;
93
+ /** Delay between polls (default 1000 ms). */
94
+ intervalMs?: number;
95
+ /** Lines of output per poll (default 200). */
96
+ tail?: number;
97
+ signal?: AbortSignal;
98
+ }
99
+ /** An agent run in a Space. */
100
+ export declare class Thread {
101
+ readonly runId: string;
102
+ readonly agent: AgentId;
103
+ readonly space: SpaceLike;
104
+ /** `space` when the thread has its Space to itself, `none` when shared. */
105
+ readonly isolation: "space" | "none";
106
+ readonly label: string | undefined;
107
+ /** Preparation steps the runtime could not complete, named. */
108
+ readonly notes: string[];
109
+ private readonly spaces;
110
+ private readonly deleteOnClose;
111
+ private readonly adapter;
112
+ private turns;
113
+ constructor(init: {
114
+ runId: string;
115
+ agent: AgentId;
116
+ space: SpaceLike;
117
+ spaces: SpacesLike;
118
+ isolation: "space" | "none";
119
+ deleteOnClose: boolean;
120
+ label?: string | undefined;
121
+ notes?: string[];
122
+ });
123
+ get id(): string;
124
+ /** The run's status on the closed ladder, with the raw output tail. */
125
+ status(tail?: number): Promise<ThreadStatus>;
126
+ /**
127
+ * Sends a follow-up. Turns on one Space are serialized. A run whose
128
+ * published `acceptsMessage` is false (a turn is running, or it could not
129
+ * be read) refuses unless `force`, and the refusal is returned, not hidden.
130
+ */
131
+ send(text: string, options?: {
132
+ force?: boolean;
133
+ }): Promise<Turn>;
134
+ /**
135
+ * Polls the run and yields derived events: text, files, links, approval
136
+ * prompts, and a `state` event on every status change. Stops once the run
137
+ * settles or after `maxPolls` (bounded).
138
+ */
139
+ events(options?: EventOptions): AsyncGenerator<ThreadEvent>;
140
+ /** Stops the run; `stopped` says whether its death was witnessed. */
141
+ stop(): Promise<{
142
+ stopped: boolean;
143
+ reason: string;
144
+ }>;
145
+ /** Stops the run and deletes a dedicated Space created for it. */
146
+ close(): Promise<void>;
147
+ }
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Agent threads: an agent CLI running a task in a Space, with an explicit
3
+ * isolation choice, a closed status ladder, and derived events.
4
+ *
5
+ * The run itself is the SDK's (`Space.agentStart` / `agentStatus` /
6
+ * `agentMessage` / `agentStop`, the same implementation as the Spaces MCP
7
+ * `agent_*` tools): a detached, tagged cua-spacesd process whose status is
8
+ * read from process liveness and its recorded exit code. This module adds
9
+ * what an app needs on top: a placement you have to choose, per-Space turn
10
+ * serialization for shared Spaces, and `events()`.
11
+ */
12
+ import { SpacesError, wrapErrors } from "./errors.js";
13
+ import { TranscriptAdapter } from "./events.js";
14
+ export const AGENT_IDS = [
15
+ "claude-code",
16
+ "gemini-cli",
17
+ "google-antigravity",
18
+ "goose",
19
+ "hermes",
20
+ "openai-codex",
21
+ "openclaw",
22
+ "opencode",
23
+ "pi",
24
+ ];
25
+ /** Statuses after which `events()` stops polling. */
26
+ export const SETTLED = new Set([
27
+ "idle",
28
+ "awaiting_input",
29
+ "finished",
30
+ "failed",
31
+ "crashed",
32
+ ]);
33
+ /** Throws unless the caller genuinely chose a placement. */
34
+ export function validatePlacement(placement) {
35
+ if (!placement || typeof placement !== "object") {
36
+ throw SpacesError.usage("a thread needs an explicit placement: { type: 'dedicated' } for its own Space, or " +
37
+ "{ type: 'shared', space, acknowledgeNoIsolation: true } to share one. There is no " +
38
+ "default because the two have different security properties.");
39
+ }
40
+ if (placement.type === "dedicated")
41
+ return placement;
42
+ if (placement.type === "shared") {
43
+ if (placement.acknowledgeNoIsolation !== true) {
44
+ throw new SpacesError("isolation", "shared placement requires acknowledgeNoIsolation: true. Threads on one Space share " +
45
+ "a filesystem, a browser profile, cookies and every app login; a screen or a window " +
46
+ "inside a Space is not a security boundary. This is the shared-coworker shape and it is a " +
47
+ "fine choice — but the SDK will not make it for you.");
48
+ }
49
+ if (!placement.space)
50
+ throw SpacesError.usage("shared placement requires a space");
51
+ return placement;
52
+ }
53
+ throw SpacesError.usage(`unknown placement type ${JSON.stringify(placement.type)}`);
54
+ }
55
+ /**
56
+ * Serializes turns per Space, shared by every thread on that Space, so two
57
+ * bots on one Space take the Space in turn instead of fighting over it.
58
+ */
59
+ export class SpaceTurnLock {
60
+ tail = Promise.resolve();
61
+ activeTurn = null;
62
+ /** The turn currently holding the Space, or null. */
63
+ get active() {
64
+ return this.activeTurn;
65
+ }
66
+ /** Queues `turnId` now (call order, not scheduling order). */
67
+ enqueue(turnId) {
68
+ const waitingBehind = this.activeTurn;
69
+ let open;
70
+ const gate = new Promise((resolve) => {
71
+ open = resolve;
72
+ });
73
+ const previous = this.tail;
74
+ // A failed turn must not wedge the Space forever.
75
+ this.tail = previous.then(() => gate, () => gate);
76
+ const acquired = previous.then(() => this.take(turnId, open), () => this.take(turnId, open));
77
+ return { waitingBehind, acquired };
78
+ }
79
+ take(turnId, open) {
80
+ this.activeTurn = turnId;
81
+ let released = false;
82
+ return () => {
83
+ if (released)
84
+ return;
85
+ released = true;
86
+ if (this.activeTurn === turnId)
87
+ this.activeTurn = null;
88
+ open();
89
+ };
90
+ }
91
+ }
92
+ const locksById = new Map();
93
+ /** One lock per Space id, so two handles to the same Space share it. */
94
+ export function lockFor(spaceId) {
95
+ let lock = locksById.get(spaceId);
96
+ if (!lock) {
97
+ lock = new SpaceTurnLock();
98
+ locksById.set(spaceId, lock);
99
+ }
100
+ return lock;
101
+ }
102
+ /** Starts an agent thread. `placement` is required and has no default. */
103
+ export async function startThread(spaces, options) {
104
+ const placement = validatePlacement(options.placement);
105
+ if (!options.prompt)
106
+ throw SpacesError.usage("startThread requires a prompt");
107
+ if (!AGENT_IDS.includes(options.agent)) {
108
+ throw SpacesError.usage(`unknown agent ${JSON.stringify(options.agent)}; known: ${AGENT_IDS.join(", ")}`);
109
+ }
110
+ return wrapErrors(async () => {
111
+ let space;
112
+ let deleteOnClose = false;
113
+ if (placement.type === "dedicated") {
114
+ const options = {
115
+ image: placement.image,
116
+ on: placement.on,
117
+ kind: placement.kind,
118
+ runtime: placement.runtime,
119
+ name: undefined,
120
+ wait: true,
121
+ reuse: false,
122
+ };
123
+ const created = await spaces.create(options);
124
+ if (!created.space)
125
+ throw SpacesError.protocol("the Space did not become ready");
126
+ space = await spaces.space(created.space.id);
127
+ deleteOnClose = placement.deleteOnClose !== false;
128
+ }
129
+ else {
130
+ space = typeof placement.space === "string" ? await spaces.space(placement.space) : placement.space;
131
+ }
132
+ const started = await space.agentStart(options.agent, options.prompt, options.show ?? false, undefined);
133
+ return new Thread({
134
+ runId: started.runId,
135
+ agent: options.agent,
136
+ space,
137
+ spaces,
138
+ isolation: placement.type === "dedicated" ? "space" : "none",
139
+ deleteOnClose,
140
+ label: options.label,
141
+ notes: started.notes,
142
+ });
143
+ });
144
+ }
145
+ /** Adopts a run that already exists (started by another process or the MCP). */
146
+ export async function adoptThread(spaces, spaceId, runId) {
147
+ return wrapErrors(async () => {
148
+ const space = await spaces.space(spaceId);
149
+ const status = await space.agentStatus(runId, 0);
150
+ if (!status.agent) {
151
+ // No run record: say so, and say why when the Space was unreachable.
152
+ const unreachable = /could not reach/.test(status.reason);
153
+ throw new SpacesError(unreachable ? "transport" : "not_found", `${runId}: ${status.reason}`);
154
+ }
155
+ return new Thread({
156
+ runId,
157
+ agent: status.agent,
158
+ space,
159
+ spaces,
160
+ isolation: "space",
161
+ deleteOnClose: false,
162
+ });
163
+ });
164
+ }
165
+ /** An agent run in a Space. */
166
+ export class Thread {
167
+ runId;
168
+ agent;
169
+ space;
170
+ /** `space` when the thread has its Space to itself, `none` when shared. */
171
+ isolation;
172
+ label;
173
+ /** Preparation steps the runtime could not complete, named. */
174
+ notes;
175
+ spaces;
176
+ deleteOnClose;
177
+ adapter = new TranscriptAdapter();
178
+ turns = 0;
179
+ constructor(init) {
180
+ this.runId = init.runId;
181
+ this.agent = init.agent;
182
+ this.space = init.space;
183
+ this.spaces = init.spaces;
184
+ this.isolation = init.isolation;
185
+ this.deleteOnClose = init.deleteOnClose;
186
+ this.label = init.label;
187
+ this.notes = init.notes ?? [];
188
+ }
189
+ get id() {
190
+ return `thread-${this.runId}`;
191
+ }
192
+ /** The run's status on the closed ladder, with the raw output tail. */
193
+ async status(tail = 200) {
194
+ const s = await wrapErrors(() => this.space.agentStatus(this.runId, tail));
195
+ return {
196
+ runId: s.runId || this.runId,
197
+ status: asStatus(s.status),
198
+ reason: s.reason,
199
+ acceptsMessage: s.acceptsMessage,
200
+ transcript: s.outputTail ?? "",
201
+ desktopWindow: null,
202
+ };
203
+ }
204
+ /**
205
+ * Sends a follow-up. Turns on one Space are serialized. A run whose
206
+ * published `acceptsMessage` is false (a turn is running, or it could not
207
+ * be read) refuses unless `force`, and the refusal is returned, not hidden.
208
+ */
209
+ async send(text, options = {}) {
210
+ if (!text)
211
+ throw SpacesError.usage("send requires text");
212
+ const turnId = `${this.runId}-turn-${++this.turns}`;
213
+ const lock = lockFor(this.space.id());
214
+ const { waitingBehind, acquired } = lock.enqueue(turnId);
215
+ const release = await acquired;
216
+ try {
217
+ if (!options.force) {
218
+ // The server's rule, never re-derived from the status word here.
219
+ const s = await wrapErrors(() => this.space.agentStatus(this.runId, 0));
220
+ if (!s.acceptsMessage) {
221
+ const turn = { id: turnId, text, state: "refused", reason: `${s.status}: ${s.reason}` };
222
+ if (waitingBehind)
223
+ turn.queuedBehind = waitingBehind;
224
+ return turn;
225
+ }
226
+ }
227
+ const r = await wrapErrors(() => this.space.agentMessage(this.runId, text, options.force ?? false));
228
+ const turn = { id: turnId, text, state: r.ok ? "delivered" : "refused", reason: r.reason };
229
+ if (waitingBehind)
230
+ turn.queuedBehind = waitingBehind;
231
+ return turn;
232
+ }
233
+ finally {
234
+ release();
235
+ }
236
+ }
237
+ /**
238
+ * Polls the run and yields derived events: text, files, links, approval
239
+ * prompts, and a `state` event on every status change. Stops once the run
240
+ * settles or after `maxPolls` (bounded).
241
+ */
242
+ async *events(options = {}) {
243
+ const maxPolls = options.maxPolls ?? 600;
244
+ const interval = options.intervalMs ?? 1000;
245
+ let last;
246
+ let seq = 0;
247
+ for (let poll = 0; poll < maxPolls; poll++) {
248
+ if (options.signal?.aborted)
249
+ return;
250
+ const s = await this.status(options.tail ?? 200);
251
+ for (const event of this.adapter.ingest(s.transcript))
252
+ yield event;
253
+ if (s.status !== last) {
254
+ last = s.status;
255
+ const state = {
256
+ kind: "state",
257
+ state: s.status,
258
+ reason: s.reason,
259
+ seq: 1_000_000 + seq++,
260
+ observedAt: new Date().toISOString(),
261
+ derived: false,
262
+ };
263
+ yield state;
264
+ }
265
+ if (SETTLED.has(s.status))
266
+ return;
267
+ await new Promise((r) => setTimeout(r, interval));
268
+ }
269
+ }
270
+ /** Stops the run; `stopped` says whether its death was witnessed. */
271
+ async stop() {
272
+ const r = await wrapErrors(() => this.space.agentStop(this.runId));
273
+ return { stopped: r.ok, reason: r.reason };
274
+ }
275
+ /** Stops the run and deletes a dedicated Space created for it. */
276
+ async close() {
277
+ await this.stop().catch(() => undefined);
278
+ if (this.deleteOnClose)
279
+ await wrapErrors(() => this.spaces.delete_(this.space.id()));
280
+ }
281
+ }
282
+ function asStatus(value) {
283
+ const known = ["running", "awaiting_input", "idle", "finished", "failed", "crashed", "unknown"];
284
+ return known.includes(value) ? value : "unknown";
285
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `McpHttpTransport`: the Spaces MCP server over streamable HTTP, as served
3
+ * by `cua daemon` at `<loopback>/mcp`.
4
+ *
5
+ * This is what a webview or browser uses when it can reach the daemon's
6
+ * loopback listener: `POST /mcp` with one JSON-RPC message, answered with
7
+ * `application/json`. The daemon mints an `Mcp-Session-Id` on `initialize`;
8
+ * every later request carries it, and a 404 means the session is gone (the
9
+ * daemon restarted), which this transport reports as a `TransportError` with
10
+ * code 404 so `McpSession` re-initializes on the next call.
11
+ *
12
+ * The bearer is the daemon's loopback token (`GetInfo.loopback_token`, or the
13
+ * `token` in `~/.cua/daemon.json`). Nothing here imports Node builtins.
14
+ */
15
+ import { type RpcOutgoing, type RpcResponse, type SendOptions, type Transport } from "./types.js";
16
+ /** The `fetch` shape this needs (the global one, or an injected fake). */
17
+ export type FetchFn = (input: string, init: {
18
+ method: string;
19
+ headers: Record<string, string>;
20
+ body?: string;
21
+ signal?: AbortSignal;
22
+ }) => Promise<{
23
+ status: number;
24
+ ok: boolean;
25
+ headers: {
26
+ get(name: string): string | null;
27
+ };
28
+ text(): Promise<string>;
29
+ }>;
30
+ export interface McpHttpOptions {
31
+ /** Daemon loopback base URL (`http://127.0.0.1:<port>`) or the full `/mcp` URL. */
32
+ url: string;
33
+ /** The daemon's loopback bearer token. */
34
+ token?: string;
35
+ /** Inject a fetch (tests, a Tauri HTTP plugin). Defaults to `globalThis.fetch`. */
36
+ fetch?: FetchFn;
37
+ }
38
+ export declare const SESSION_HEADER = "mcp-session-id";
39
+ export declare class McpHttpTransport implements Transport {
40
+ readonly kind = "http";
41
+ private readonly endpoint;
42
+ private readonly token;
43
+ private readonly doFetch;
44
+ private session;
45
+ constructor(options: McpHttpOptions);
46
+ /** The session id the daemon minted, once initialized. */
47
+ get sessionId(): string | undefined;
48
+ send(message: RpcOutgoing, options?: SendOptions): Promise<RpcResponse | null>;
49
+ /** Ends the MCP session (`DELETE /mcp`). Never throws. */
50
+ close(): Promise<void>;
51
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * `McpHttpTransport`: the Spaces MCP server over streamable HTTP, as served
3
+ * by `cua daemon` at `<loopback>/mcp`.
4
+ *
5
+ * This is what a webview or browser uses when it can reach the daemon's
6
+ * loopback listener: `POST /mcp` with one JSON-RPC message, answered with
7
+ * `application/json`. The daemon mints an `Mcp-Session-Id` on `initialize`;
8
+ * every later request carries it, and a 404 means the session is gone (the
9
+ * daemon restarted), which this transport reports as a `TransportError` with
10
+ * code 404 so `McpSession` re-initializes on the next call.
11
+ *
12
+ * The bearer is the daemon's loopback token (`GetInfo.loopback_token`, or the
13
+ * `token` in `~/.cua/daemon.json`). Nothing here imports Node builtins.
14
+ */
15
+ import { TransportError, } from "./types.js";
16
+ export const SESSION_HEADER = "mcp-session-id";
17
+ export class McpHttpTransport {
18
+ kind = "http";
19
+ endpoint;
20
+ token;
21
+ doFetch;
22
+ session;
23
+ constructor(options) {
24
+ const base = options.url.replace(/\/+$/, "");
25
+ this.endpoint = base.endsWith("/mcp") ? base : `${base}/mcp`;
26
+ this.token = options.token;
27
+ const f = options.fetch ?? globalThis.fetch;
28
+ if (!f)
29
+ throw new TransportError("no fetch available; pass options.fetch");
30
+ this.doFetch = f;
31
+ }
32
+ /** The session id the daemon minted, once initialized. */
33
+ get sessionId() {
34
+ return this.session;
35
+ }
36
+ async send(message, options = {}) {
37
+ const headers = {
38
+ "content-type": "application/json",
39
+ accept: "application/json, text/event-stream",
40
+ };
41
+ if (this.token)
42
+ headers.authorization = `Bearer ${this.token}`;
43
+ if (this.session && message.method !== "initialize")
44
+ headers[SESSION_HEADER] = this.session;
45
+ const controller = new AbortController();
46
+ const timer = options.timeoutSeconds === undefined
47
+ ? undefined
48
+ : setTimeout(() => controller.abort(), options.timeoutSeconds * 1000);
49
+ const onAbort = () => controller.abort();
50
+ options.signal?.addEventListener("abort", onAbort, { once: true });
51
+ let response;
52
+ try {
53
+ response = await this.doFetch(this.endpoint, {
54
+ method: "POST",
55
+ headers,
56
+ body: JSON.stringify(message),
57
+ signal: controller.signal,
58
+ });
59
+ }
60
+ catch (cause) {
61
+ throw new TransportError(`${message.method}: the daemon at ${this.endpoint} did not answer (${String(cause)})`);
62
+ }
63
+ finally {
64
+ if (timer)
65
+ clearTimeout(timer);
66
+ options.signal?.removeEventListener("abort", onAbort);
67
+ }
68
+ const text = await response.text();
69
+ if (response.status === 404 && this.session) {
70
+ // The daemon forgot the session (it restarted): make the next call
71
+ // initialize again.
72
+ this.session = undefined;
73
+ throw new TransportError(`${message.method}: MCP session expired`, 404);
74
+ }
75
+ if (response.status === 401) {
76
+ throw new TransportError(`${message.method}: the daemon refused the bearer token`, 401);
77
+ }
78
+ if (!response.ok && response.status !== 202) {
79
+ throw new TransportError(`${message.method}: HTTP ${response.status} ${text.slice(0, 200)}`, response.status);
80
+ }
81
+ const minted = response.headers.get(SESSION_HEADER);
82
+ if (minted)
83
+ this.session = minted;
84
+ if (!("id" in message) || text.trim() === "")
85
+ return null;
86
+ let parsed;
87
+ try {
88
+ parsed = JSON.parse(text);
89
+ }
90
+ catch {
91
+ throw new TransportError(`${message.method}: the daemon returned non-JSON: ${text.slice(0, 200)}`);
92
+ }
93
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
94
+ throw new TransportError(`${message.method}: the daemon returned a non-object reply`);
95
+ }
96
+ return parsed;
97
+ }
98
+ /** Ends the MCP session (`DELETE /mcp`). Never throws. */
99
+ async close() {
100
+ if (!this.session)
101
+ return;
102
+ const headers = { [SESSION_HEADER]: this.session };
103
+ if (this.token)
104
+ headers.authorization = `Bearer ${this.token}`;
105
+ this.session = undefined;
106
+ try {
107
+ await this.doFetch(this.endpoint, { method: "DELETE", headers });
108
+ }
109
+ catch {
110
+ /* the daemon is gone, which is the state we wanted */
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `@trycua/cua/spaces/transport`: the Spaces control plane for hosts that
3
+ * cannot load the native binding (a Tauri webview, a browser).
4
+ *
5
+ * Dependency-free TypeScript: `McpSession` speaks MCP to the Rust Spaces
6
+ * server over any `Transport`:
7
+ *
8
+ * - `McpHttpTransport`: the `cua daemon` loopback `/mcp` (streamable HTTP,
9
+ * bearer = the daemon's loopback token);
10
+ * - `TauriTransport`: `invoke()` into the Tauri app's Rust side.
11
+ *
12
+ * Node code should use `@trycua/cua/spaces` (the typed SDK) instead. See
13
+ * `types.ts` for why the seam exchanges whole JSON-RPC messages rather than
14
+ * bytes.
15
+ */
16
+ export { type Json, type RpcError, type RpcId, type RpcNotification, type RpcOutgoing, type RpcRequest, type RpcResponse, type SendOptions, type Transport, TransportError, } from "./types.js";
17
+ export { type ClientInfo, type ContentPart, McpSession, PROTOCOL_VERSION, type ServerInfo, type SessionOptions, ToolError, type ToolDescriptor, type ToolResult, textOf, } from "./session.js";
18
+ export { DEFAULT_REQUEST_COMMAND, DEFAULT_SHUTDOWN_COMMAND, type InvokeFn, TauriTransport, type TauriOptions, } from "./tauri.js";
19
+ export { type FetchFn, McpHttpTransport, type McpHttpOptions, SESSION_HEADER } from "./http.js";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `@trycua/cua/spaces/transport`: the Spaces control plane for hosts that
3
+ * cannot load the native binding (a Tauri webview, a browser).
4
+ *
5
+ * Dependency-free TypeScript: `McpSession` speaks MCP to the Rust Spaces
6
+ * server over any `Transport`:
7
+ *
8
+ * - `McpHttpTransport`: the `cua daemon` loopback `/mcp` (streamable HTTP,
9
+ * bearer = the daemon's loopback token);
10
+ * - `TauriTransport`: `invoke()` into the Tauri app's Rust side.
11
+ *
12
+ * Node code should use `@trycua/cua/spaces` (the typed SDK) instead. See
13
+ * `types.ts` for why the seam exchanges whole JSON-RPC messages rather than
14
+ * bytes.
15
+ */
16
+ export { TransportError, } from "./types.js";
17
+ export { McpSession, PROTOCOL_VERSION, ToolError, textOf, } from "./session.js";
18
+ export { DEFAULT_REQUEST_COMMAND, DEFAULT_SHUTDOWN_COMMAND, TauriTransport, } from "./tauri.js";
19
+ export { McpHttpTransport, SESSION_HEADER } from "./http.js";