@sealant/sdk 0.7.1 → 0.8.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.
@@ -1,4 +1,95 @@
1
1
  import { closeSessionOp, getSessionOp, getSessionOutputOp, resizeSessionOp, sendSessionInputOp, signalSessionOp, } from "../effect/operations.js";
2
+ /**
3
+ * Open the held-WebSocket terminal attachment (the data plane). One socket:
4
+ * binary frames are PTY bytes in both directions, text frames are control
5
+ * JSON (`{"t":"resize",...}` up, `{"t":"end"}` down). Auth rides the connect —
6
+ * `?token=` for apiKey clients (WebSocket cannot set headers), `?ownerUserId=`
7
+ * for host-local — and never repeats per event.
8
+ */
9
+ const openAttachment = (ctx, sessionId, options) => {
10
+ const config = ctx.config;
11
+ const url = new URL(`/v1/sessions/${sessionId}/attach`, config.baseUrl);
12
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
13
+ url.searchParams.set("from", (options?.from ?? 0n).toString());
14
+ if (config.apiKey === undefined) {
15
+ url.searchParams.set("ownerUserId", config.hostLocal.ownerUserId);
16
+ }
17
+ else {
18
+ url.searchParams.set("token", config.apiKey);
19
+ }
20
+ return new Promise((resolve, reject) => {
21
+ const ws = new WebSocket(url);
22
+ ws.binaryType = "arraybuffer";
23
+ // Push-queue bridging WS message events to the pull-based async iterable.
24
+ const pending = [];
25
+ let wake;
26
+ let finished = false;
27
+ const closedResolver = Promise.withResolvers();
28
+ const closed = closedResolver.promise;
29
+ const finish = (reason) => {
30
+ if (finished) {
31
+ return;
32
+ }
33
+ finished = true;
34
+ closedResolver.resolve(reason);
35
+ wake?.();
36
+ };
37
+ ws.addEventListener("message", (event) => {
38
+ if (typeof event.data === "string") {
39
+ try {
40
+ const frame = JSON.parse(event.data);
41
+ if (frame.t === "end") {
42
+ finish("end");
43
+ }
44
+ }
45
+ catch {
46
+ // Unknown text frame — ignore.
47
+ }
48
+ return;
49
+ }
50
+ pending.push(new Uint8Array(event.data));
51
+ wake?.();
52
+ });
53
+ ws.addEventListener("close", () => finish("closed"));
54
+ const output = {
55
+ [Symbol.asyncIterator]: () => ({
56
+ next: async () => {
57
+ for (;;) {
58
+ const chunk = pending.shift();
59
+ if (chunk !== undefined) {
60
+ return { done: false, value: chunk };
61
+ }
62
+ if (finished) {
63
+ return { done: true, value: undefined };
64
+ }
65
+ await new Promise((r) => {
66
+ wake = r;
67
+ });
68
+ wake = undefined;
69
+ }
70
+ },
71
+ }),
72
+ };
73
+ const attachment = {
74
+ send: (input) => {
75
+ const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
76
+ // Copy into a plain ArrayBuffer-backed view (WebSocket.send rejects SharedArrayBuffer views).
77
+ ws.send(new Uint8Array(bytes).buffer);
78
+ },
79
+ resize: (cols, rows) => {
80
+ ws.send(JSON.stringify({ t: "resize", cols, rows }));
81
+ },
82
+ output,
83
+ closed,
84
+ close: () => {
85
+ finish("closed");
86
+ ws.close();
87
+ },
88
+ };
89
+ ws.addEventListener("open", () => resolve(attachment), { once: true });
90
+ ws.addEventListener("error", () => reject(new Error(`session attach failed: could not connect to ${url.host}`)), { once: true });
91
+ });
92
+ };
2
93
  const OUTPUT_POLL_INTERVAL_MS = 250;
3
94
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4
95
  const decodeBase64 = (value) => {
@@ -167,5 +258,6 @@ export const makeInteractiveSession = (ctx, wire) => {
167
258
  close: async () => {
168
259
  await ctx.runtime.run(closeSessionOp(sessionId, { ownerUserId: ctx.config.hostLocal.ownerUserId }));
169
260
  },
261
+ attach: (options) => openAttachment(ctx, sessionId, options),
170
262
  };
171
263
  };
package/dist/types.d.ts CHANGED
@@ -559,6 +559,35 @@ export interface InteractiveSession {
559
559
  status(): Promise<InteractiveSessionStatus>;
560
560
  /** Close the PTY (hang up the terminal). Resolves once the session settles. */
561
561
  close(): Promise<void>;
562
+ /**
563
+ * THE DATA PLANE for interactive terminals: one held WebSocket carrying
564
+ * input, output, and resize — auth once at connect, no per-keystroke
565
+ * requests. Output replays byte-exact from `from` and then live-tails.
566
+ * `send`/`resize`/`signal`/`output` above remain the request/response
567
+ * control-plane verbs; a terminal UI should attach instead.
568
+ */
569
+ attach(options?: SessionAttachOptions): Promise<SessionAttachment>;
570
+ }
571
+ /** Options for {@link InteractiveSession.attach}. */
572
+ export interface SessionAttachOptions {
573
+ /** Replay output from this sequence (inclusive; default `0n` = full history). */
574
+ readonly from?: bigint;
575
+ }
576
+ /**
577
+ * A live terminal attachment — one WebSocket, held until `close()` or the
578
+ * session settles. Not durable: reattach by calling `attach` again.
579
+ */
580
+ export interface SessionAttachment {
581
+ /** Write keystrokes onto the held socket (no request/response round-trip). */
582
+ send(input: string | Uint8Array): void;
583
+ /** Resize the PTY over the held socket. */
584
+ resize(cols: number, rows: number): void;
585
+ /** Output bytes: recorded replay from `from`, then live, until settle/close. */
586
+ readonly output: AsyncIterable<Uint8Array>;
587
+ /** Resolves when the attachment ends: session settled (`"end"`) or the socket closed. */
588
+ readonly closed: Promise<"end" | "closed">;
589
+ /** Drop the attachment (the session keeps running). */
590
+ close(): void;
562
591
  }
563
592
  /** Interactive sessions of one workspace: open new ones, reattach to existing ones. */
564
593
  export interface WorkspaceSessions {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealant/sdk",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "The fluent public SDK for Sealant — create a workspace, run a harness, replay the record.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "effect": "^4.0.0-beta.85",
30
- "@sealant/api-contracts": "^0.7.1"
30
+ "@sealant/api-contracts": "^0.8.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@effect/vitest": "^4.0.0-beta.85",