@myrialabs/ptykit 0.0.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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +260 -0
  3. package/dist/client/fit.d.ts +29 -0
  4. package/dist/client/fit.d.ts.map +1 -0
  5. package/dist/client/fit.js +45 -0
  6. package/dist/client/fit.js.map +1 -0
  7. package/dist/client/index.d.ts +10 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +9 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/client/persistence.d.ts +15 -0
  12. package/dist/client/persistence.d.ts.map +1 -0
  13. package/dist/client/persistence.js +47 -0
  14. package/dist/client/persistence.js.map +1 -0
  15. package/dist/client/pty-kit-client.d.ts +122 -0
  16. package/dist/client/pty-kit-client.d.ts.map +1 -0
  17. package/dist/client/pty-kit-client.js +245 -0
  18. package/dist/client/pty-kit-client.js.map +1 -0
  19. package/dist/client/terminal.d.ts +77 -0
  20. package/dist/client/terminal.d.ts.map +1 -0
  21. package/dist/client/terminal.js +112 -0
  22. package/dist/client/terminal.js.map +1 -0
  23. package/dist/client/ws-core.d.ts +88 -0
  24. package/dist/client/ws-core.d.ts.map +1 -0
  25. package/dist/client/ws-core.js +324 -0
  26. package/dist/client/ws-core.js.map +1 -0
  27. package/dist/core/backend.d.ts +52 -0
  28. package/dist/core/backend.d.ts.map +1 -0
  29. package/dist/core/backend.js +11 -0
  30. package/dist/core/backend.js.map +1 -0
  31. package/dist/core/detect.d.ts +21 -0
  32. package/dist/core/detect.d.ts.map +1 -0
  33. package/dist/core/detect.js +82 -0
  34. package/dist/core/detect.js.map +1 -0
  35. package/dist/core/env.d.ts +30 -0
  36. package/dist/core/env.d.ts.map +1 -0
  37. package/dist/core/env.js +68 -0
  38. package/dist/core/env.js.map +1 -0
  39. package/dist/core/index.d.ts +11 -0
  40. package/dist/core/index.d.ts.map +1 -0
  41. package/dist/core/index.js +10 -0
  42. package/dist/core/index.js.map +1 -0
  43. package/dist/core/pty-kit.d.ts +90 -0
  44. package/dist/core/pty-kit.d.ts.map +1 -0
  45. package/dist/core/pty-kit.js +187 -0
  46. package/dist/core/pty-kit.js.map +1 -0
  47. package/dist/core/scrollback.d.ts +43 -0
  48. package/dist/core/scrollback.d.ts.map +1 -0
  49. package/dist/core/scrollback.js +79 -0
  50. package/dist/core/scrollback.js.map +1 -0
  51. package/dist/core/session.d.ts +100 -0
  52. package/dist/core/session.d.ts.map +1 -0
  53. package/dist/core/session.js +264 -0
  54. package/dist/core/session.js.map +1 -0
  55. package/dist/core/shell.d.ts +24 -0
  56. package/dist/core/shell.d.ts.map +1 -0
  57. package/dist/core/shell.js +55 -0
  58. package/dist/core/shell.js.map +1 -0
  59. package/dist/index.d.ts +11 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +11 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/server/connection.d.ts +38 -0
  64. package/dist/server/connection.d.ts.map +1 -0
  65. package/dist/server/connection.js +67 -0
  66. package/dist/server/connection.js.map +1 -0
  67. package/dist/server/ids.d.ts +2 -0
  68. package/dist/server/ids.d.ts.map +1 -0
  69. package/dist/server/ids.js +7 -0
  70. package/dist/server/ids.js.map +1 -0
  71. package/dist/server/index.d.ts +6 -0
  72. package/dist/server/index.d.ts.map +1 -0
  73. package/dist/server/index.js +6 -0
  74. package/dist/server/index.js.map +1 -0
  75. package/dist/server/pty-kit-server.d.ts +101 -0
  76. package/dist/server/pty-kit-server.d.ts.map +1 -0
  77. package/dist/server/pty-kit-server.js +361 -0
  78. package/dist/server/pty-kit-server.js.map +1 -0
  79. package/dist/server/transport-bun.d.ts +26 -0
  80. package/dist/server/transport-bun.d.ts.map +1 -0
  81. package/dist/server/transport-bun.js +79 -0
  82. package/dist/server/transport-bun.js.map +1 -0
  83. package/dist/server/transport-node.d.ts +20 -0
  84. package/dist/server/transport-node.d.ts.map +1 -0
  85. package/dist/server/transport-node.js +77 -0
  86. package/dist/server/transport-node.js.map +1 -0
  87. package/dist/shared/index.d.ts +260 -0
  88. package/dist/shared/index.d.ts.map +1 -0
  89. package/dist/shared/index.js +85 -0
  90. package/dist/shared/index.js.map +1 -0
  91. package/package.json +108 -0
  92. package/src/client/svelte/PtyTerminal.svelte +146 -0
  93. package/src/client/svelte/index.d.ts +84 -0
  94. package/src/client/svelte/index.js +4 -0
  95. package/src/client/svelte/svelte-compile.test.ts +11 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Myria Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,260 @@
1
+ <p align="center">
2
+ <img src="https://ptykit.myrialabs.dev/favicon.svg" alt="PtyKit" width="72" height="72" />
3
+ </p>
4
+
5
+ <h1 align="center">PtyKit</h1>
6
+
7
+ <p align="center">
8
+ <strong>PTY sessions over WebSocket for Node &amp; Bun.</strong><br />
9
+ Collaborative rooms, serialized-scrollback reattach, and a resilient browser
10
+ client — one typed API.
11
+ </p>
12
+
13
+ <p align="center">
14
+ <a href="https://ptykit.myrialabs.dev">Website</a> ·
15
+ <a href="https://www.npmjs.com/package/@myrialabs/ptykit">npm</a> ·
16
+ <a href="./docs/api.md">API reference</a> ·
17
+ <a href="./docs/server.md">Server</a> ·
18
+ <a href="./docs/client.md">Client</a> ·
19
+ <a href="./examples/README.md">Examples</a> ·
20
+ <a href="https://github.com/myrialabs/ptykit/issues">Issues</a>
21
+ </p>
22
+
23
+ <p align="center">
24
+ <a href="https://www.npmjs.com/package/@myrialabs/ptykit"><img src="https://img.shields.io/npm/v/@myrialabs/ptykit" alt="npm version" /></a>
25
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT" /></a>
26
+ <img src="https://img.shields.io/badge/runtime-Node%2018%2B%20%7C%20Bun-black" alt="Node 18+ and Bun" />
27
+ </p>
28
+
29
+ ---
30
+
31
+ PtyKit runs interactive shells server-side and streams them to the browser over a
32
+ single WebSocket. Output is kept in a headless terminal so a refresh, a dropped
33
+ connection, or a second viewer all replay the exact screen — no lost bytes, no
34
+ double output. The PTY backend is auto-detected (`bun-pty` on Bun, `node-pty` on
35
+ Node); you bring the auth.
36
+
37
+ ```ts
38
+ // Server
39
+ import { PtyKit, createPtyKitServer } from '@myrialabs/ptykit';
40
+
41
+ const manager = new PtyKit();
42
+ const server = createPtyKitServer(manager, {
43
+ path: '/pty',
44
+ authorize: (ctx) => ctx.conn.data.user?.canAccess(ctx.namespace) ?? false,
45
+ });
46
+
47
+ Bun.serve({ port: 3000, fetch: server.fetch, websocket: server.websocket });
48
+ ```
49
+
50
+ ```ts
51
+ // Browser
52
+ import { PtyKitClient } from '@myrialabs/ptykit/client';
53
+
54
+ const client = new PtyKitClient({ url: '/pty', namespace: 'project-42' });
55
+ const session = await client.attach('project-42-terminal-1');
56
+ session.onData((chunk) => term.write(chunk));
57
+ term.onData((data) => session.write(data));
58
+ ```
59
+
60
+ ## Why PtyKit
61
+
62
+ - **WebSocket only** — one multiplexed control + data channel. No SSE, no
63
+ transport option, no polling.
64
+ - **Collaborative rooms** — output broadcasts to a room (default = namespace), so
65
+ **N clients ↔ 1 session**. Multiple viewers see the same live terminal.
66
+ - **Reattach that just works** — scrollback lives server-side in a headless
67
+ xterm and replays as a single serialized frame. Survives refresh, disconnect,
68
+ and tab switches with **zero data loss** and no double output.
69
+ - **Auto-detected backend** — `bun-pty` on Bun (the tested path), `node-pty` on
70
+ Node (experimental). A Node consumer never builds bun-pty's rust/ffi, and
71
+ vice-versa — both are optional, lazily loaded.
72
+ - **Resilient client** — reconnect with exponential backoff, heal-reconnect for
73
+ "open but dead" sockets, and idempotency-aware resend, all on by default.
74
+ - **Bring your own auth** — an `authorize` hook enforces namespace access with
75
+ anti-hijack ownership checks. The library ships no auth of its own.
76
+ - **Quiet, typed core** — no stdout/stderr writes, `sideEffects: false`, JSDoc on
77
+ every export, runs on Node 18+ and Bun.
78
+
79
+ ## Install
80
+
81
+ ```sh
82
+ bun add @myrialabs/ptykit # or: npm i @myrialabs/ptykit / pnpm add @myrialabs/ptykit
83
+ ```
84
+
85
+ The PTY backend is an **optional** dependency resolved at runtime: `bun-pty` on
86
+ Bun, `node-pty` on Node. For the Node WebSocket server, `ws` is used (also
87
+ optional). Browser peers (`@xterm/xterm`, `@xterm/addon-fit`) and `svelte` are
88
+ optional peer dependencies you already have in a frontend.
89
+
90
+ ## Entry points
91
+
92
+ | Import | What |
93
+ | --- | --- |
94
+ | `@myrialabs/ptykit` | Core session engine (`PtyKit`) + WebSocket server (`createPtyKitServer`). |
95
+ | `@myrialabs/ptykit/client` | Framework-agnostic browser client (`mountTerminal`, `PtyKitClient`, `attachFit`). |
96
+ | `@myrialabs/ptykit/svelte` | Official Svelte component (`<PtyTerminal/>`). |
97
+
98
+ ## Quick start
99
+
100
+ | Task | API |
101
+ | --- | --- |
102
+ | Create the manager | `const m = new PtyKit({ scrollback: 5000 })` |
103
+ | Mount on Bun | `Bun.serve({ fetch: server.fetch, websocket: server.websocket })` |
104
+ | Mount on Node | `await server.attach(httpServer)` |
105
+ | Terminal (vanilla) | `await mountTerminal(el, { url: '/pty', namespace, sessionId, create: true })` |
106
+ | Attach (client) | `await client.attach(sessionId)` |
107
+ | Create (client) | `await client.create({ cols, rows })` |
108
+ | Stream output | `session.onData((chunk) => term.write(chunk))` |
109
+ | Send keystrokes | `session.write(data)` |
110
+ | Resize | `attachFit(session, term, fitAddon)` |
111
+ | Svelte | `<PtyTerminal {sessionId} url="/pty" namespace="project-42" />` |
112
+
113
+ ## Server
114
+
115
+ `createPtyKitServer` mounts onto the HTTP server you already have.
116
+
117
+ **Bun** — wire `fetch` + `websocket` into `Bun.serve`:
118
+
119
+ ```ts
120
+ Bun.serve({ port: 3000, fetch: server.fetch, websocket: server.websocket });
121
+ ```
122
+
123
+ **Node** — attach to an `http.Server` (uses the optional `ws` package):
124
+
125
+ ```ts
126
+ import http from 'node:http';
127
+ const httpServer = http.createServer(app);
128
+ await server.attach(httpServer);
129
+ httpServer.listen(3000);
130
+ ```
131
+
132
+ The `PtyKit` manager owns the sessions and is transport-agnostic:
133
+
134
+ ```ts
135
+ const manager = new PtyKit({
136
+ scrollback: 5000, // headless xterm lines
137
+ idleTtl: null, // sessions live until killed; a number opts into idle reaping
138
+ retainExitedMs: 5 * 60_000, // keep exited sessions this long for reconnect replay
139
+ env: { sanitize: true, inject: { MY_VAR: '1' } }, // strip runtime pollution, inject yours
140
+ });
141
+ ```
142
+
143
+ See [docs/server.md](./docs/server.md) for the full surface, operations, and events.
144
+
145
+ ## Collaborative rooms
146
+
147
+ Output broadcasts to a room (default = the namespace) and clients filter by
148
+ `sessionId`, so any number of clients can attach to the same session and watch
149
+ the same live terminal. The serialized reattach frame is unicast to the joining
150
+ client, so existing viewers are never repainted.
151
+
152
+ ```ts
153
+ const server = createPtyKitServer(manager, {
154
+ room: (ctx) => ctx.namespace, // or group however you like
155
+ });
156
+ ```
157
+
158
+ ## The resilient client
159
+
160
+ Skip the xterm boilerplate with `mountTerminal` — the framework-agnostic
161
+ counterpart to `<PtyTerminal/>`. Give it a container and a url; it creates the
162
+ terminal, fits it, opens the session, and wires output⇄input, while staying fully
163
+ configurable.
164
+
165
+ ```ts
166
+ import { mountTerminal } from '@myrialabs/ptykit/client';
167
+
168
+ const { session, terminal, dispose } = await mountTerminal(el, {
169
+ url: '/pty',
170
+ namespace: 'project-42',
171
+ sessionId: 'project-42-terminal-1',
172
+ create: true,
173
+ onStatus: (s) => render(s),
174
+ });
175
+ ```
176
+
177
+ Need full control? Drop down to `PtyKitClient`. Reconnect is on by default —
178
+ exponential backoff (1s → 30s), heal-reconnect for sockets that are "open but
179
+ dead", and idempotency-aware resend. On reconnect, every known session is
180
+ re-attached so the room subscription and scrollback recover.
181
+
182
+ ```ts
183
+ const client = new PtyKitClient({
184
+ url: '/pty',
185
+ namespace: 'project-42',
186
+ reconnect: { enabled: true, baseDelayMs: 1000, maxDelayMs: 30_000, maxAttempts: 5 },
187
+ persistence: { load, save }, // optional: own the active-session id yourself
188
+ });
189
+
190
+ client.onStatus((s) => render(s)); // 'connected' | 'reconnecting' | 'disconnected'
191
+ ```
192
+
193
+ See [docs/client.md](./docs/client.md) for `mountTerminal`, `attachFit`,
194
+ persistence, and the session API.
195
+
196
+ ## Svelte
197
+
198
+ ```svelte
199
+ <script>
200
+ import { PtyTerminal } from '@myrialabs/ptykit/svelte';
201
+ </script>
202
+
203
+ <PtyTerminal sessionId="project-42-terminal-1" url="/pty" namespace="project-42" />
204
+ ```
205
+
206
+ The component is fully configurable (theme, font, reconnect, lifecycle
207
+ callbacks, …). See [docs/svelte.md](./docs/svelte.md).
208
+
209
+ ## Security
210
+
211
+ The `authorize` hook **defaults to allow-all** so the package is friendly to try
212
+ locally — this is unsafe in production. A network-reachable deployment must
213
+ provide an `authorize` implementation that checks the connection's identity
214
+ (populated by `onUpgrade`) against the requested `namespace`. PtyKit also rejects
215
+ cross-namespace `sessionId` access (anti-hijack), but it cannot know who your
216
+ users are — that's your hook's job.
217
+
218
+ ## node-pty status
219
+
220
+ `bun-pty` is the default, tested backend. The `node-pty` adapter exists and
221
+ auto-activates under Node, but is marked **experimental** until the scale and
222
+ auto-detect benchmarks gate it. On the benchmark machine (Node 25, macOS arm64),
223
+ node-pty failed to spawn (`posix_spawnp`, reproduced with raw node-pty) — see
224
+ [bench-results.md](./bench-results.md).
225
+
226
+ ## Performance
227
+
228
+ Measured on a dev laptop (Apple M2, Bun 1.3.14). Reproduce with `bun bench.ts`;
229
+ full numbers in [bench-results.md](./bench-results.md).
230
+
231
+ - **Throughput overhead** of the wrapped pipeline vs raw bun-pty: **~7%**
232
+ (target <10%) — the cost of persist-to-headless-first + batching.
233
+ - **Reattach** serialize latency: p50 ~3–9ms, p95 ~10–19ms across 10KB/100KB/1MB
234
+ buffers; the newest output is always present.
235
+ - **Idle footprint**: ~0.13 MB of parent-process RSS per session. In-memory
236
+ scrollback was fine at 100 sessions — no disk spill needed.
237
+
238
+ ## Documentation
239
+
240
+ - [API reference](./docs/api.md) — every export across the three entry points.
241
+ - [Server](./docs/server.md) — `PtyKit`, `createPtyKitServer`, rooms, `authorize`.
242
+ - [Client](./docs/client.md) — `mountTerminal`, `PtyKitClient`, reconnect, persistence, `attachFit`.
243
+ - [Svelte](./docs/svelte.md) — the `<PtyTerminal/>` component.
244
+ - [Examples](./examples/README.md) — runnable scenarios.
245
+
246
+ ## Support
247
+
248
+ If PtyKit is useful to you, consider supporting its development:
249
+
250
+ | Method | Address / Link |
251
+ |--------|----------------|
252
+ | Bitcoin (BTC) | `bc1qd9fyx4r84cce2a9hkjksetah802knadw5msls3` |
253
+ | Solana (SOL) | `Ev3P4KLF1PNC5C9rZYP8M3DdssyBQAQAiNJkvNmPQPVs` |
254
+ | Ethereum (ERC-20) | `0x61D826e5b666AA5345302EEEd485Acca39b1AFCF` |
255
+ | USDT (TRC-20) | `TLH49i3EoVKhFyLb6u2JUXZWScK7uzksdC` |
256
+ | Saweria | [saweria.co/myrialabs](https://saweria.co/myrialabs) |
257
+
258
+ ## License
259
+
260
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `attachFit` (R15) — bind an xterm `FitAddon` + `ResizeObserver` to a session.
3
+ *
4
+ * Fits the terminal to its container on resize (debounced, default 100ms),
5
+ * skips redundant resizes, and forwards the new dimensions to the server.
6
+ */
7
+ import type { ClientSession } from './pty-kit-client.js';
8
+ /** Minimal xterm Terminal shape we depend on. */
9
+ interface TerminalLike {
10
+ element?: HTMLElement | undefined;
11
+ }
12
+ /** Minimal FitAddon shape we depend on. */
13
+ interface FitAddonLike {
14
+ fit(): void;
15
+ proposeDimensions(): {
16
+ cols: number;
17
+ rows: number;
18
+ } | undefined;
19
+ }
20
+ export interface AttachFitOptions {
21
+ debounceMs?: number;
22
+ }
23
+ /**
24
+ * Observe the terminal's container, fit on resize, and send `resize` to the
25
+ * server. Returns a disposer that stops observing.
26
+ */
27
+ export declare function attachFit(session: Pick<ClientSession, 'resize'>, term: TerminalLike, fitAddon: FitAddonLike, options?: AttachFitOptions): () => void;
28
+ export {};
29
+ //# sourceMappingURL=fit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fit.d.ts","sourceRoot":"","sources":["../../src/client/fit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,iDAAiD;AACjD,UAAU,YAAY;IACrB,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CAClC;AACD,2CAA2C;AAC3C,UAAU,YAAY;IACrB,GAAG,IAAI,IAAI,CAAC;IACZ,iBAAiB,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CAChE;AAED,MAAM,WAAW,gBAAgB;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACxB,OAAO,EAAE,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,EACtC,IAAI,EAAE,YAAY,EAClB,QAAQ,EAAE,YAAY,EACtB,OAAO,GAAE,gBAAqB,GAC5B,MAAM,IAAI,CAiCZ"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `attachFit` (R15) — bind an xterm `FitAddon` + `ResizeObserver` to a session.
3
+ *
4
+ * Fits the terminal to its container on resize (debounced, default 100ms),
5
+ * skips redundant resizes, and forwards the new dimensions to the server.
6
+ */
7
+ /**
8
+ * Observe the terminal's container, fit on resize, and send `resize` to the
9
+ * server. Returns a disposer that stops observing.
10
+ */
11
+ export function attachFit(session, term, fitAddon, options = {}) {
12
+ const debounceMs = options.debounceMs ?? 100;
13
+ let timer = null;
14
+ let last = null;
15
+ const run = () => {
16
+ if (timer)
17
+ clearTimeout(timer);
18
+ timer = setTimeout(() => {
19
+ try {
20
+ fitAddon.fit();
21
+ const dims = fitAddon.proposeDimensions();
22
+ if (!dims)
23
+ return;
24
+ if (last && last.cols === dims.cols && last.rows === dims.rows)
25
+ return;
26
+ last = { cols: dims.cols, rows: dims.rows };
27
+ void session.resize(dims.cols, dims.rows);
28
+ }
29
+ catch {
30
+ /* container may have zero dimensions; will retry on next resize */
31
+ }
32
+ }, debounceMs);
33
+ };
34
+ const target = term.element?.parentElement ?? term.element ?? null;
35
+ const observer = typeof ResizeObserver !== 'undefined' && target ? new ResizeObserver(run) : null;
36
+ observer?.observe(target);
37
+ // Initial fit.
38
+ run();
39
+ return () => {
40
+ if (timer)
41
+ clearTimeout(timer);
42
+ observer?.disconnect();
43
+ };
44
+ }
45
+ //# sourceMappingURL=fit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fit.js","sourceRoot":"","sources":["../../src/client/fit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAkBH;;;GAGG;AACH,MAAM,UAAU,SAAS,CACxB,OAAsC,EACtC,IAAkB,EAClB,QAAsB,EACtB,UAA4B,EAAE;IAE9B,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,GAAG,CAAC;IAC7C,IAAI,KAAK,GAAyC,IAAI,CAAC;IACvD,IAAI,IAAI,GAA0C,IAAI,CAAC;IAEvD,MAAM,GAAG,GAAG,GAAG,EAAE;QAChB,IAAI,KAAK;YAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QAC/B,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YACvB,IAAI,CAAC;gBACJ,QAAQ,CAAC,GAAG,EAAE,CAAC;gBACf,MAAM,IAAI,GAAG,QAAQ,CAAC,iBAAiB,EAAE,CAAC;gBAC1C,IAAI,CAAC,IAAI;oBAAE,OAAO;gBAClB,IAAI,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI;oBAAE,OAAO;gBACvE,IAAI,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;gBAC5C,KAAK,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3C,CAAC;YAAC,MAAM,CAAC;gBACR,mEAAmE;YACpE,CAAC;QACF,CAAC,EAAE,UAAU,CAAC,CAAC;IAChB,CAAC,CAAC;IAEF,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,aAAa,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC;IACnE,MAAM,QAAQ,GACb,OAAO,cAAc,KAAK,WAAW,IAAI,MAAM,CAAC,CAAC,CAAC,IAAI,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAClF,QAAQ,EAAE,OAAO,CAAC,MAAiB,CAAC,CAAC;IAErC,eAAe;IACf,GAAG,EAAE,CAAC;IAEN,OAAO,GAAG,EAAE;QACX,IAAI,KAAK;YAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QAC/B,QAAQ,EAAE,UAAU,EAAE,CAAC;IACxB,CAAC,CAAC;AACH,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `ptykit/client` — framework-agnostic browser client.
3
+ */
4
+ export { PtyKitClient, ClientSession, type PtyKitClientOptions, type OpenOptions, } from './pty-kit-client.js';
5
+ export { attachFit, type AttachFitOptions } from './fit.js';
6
+ export { mountTerminal, type MountTerminalOptions, type TerminalHandle, } from './terminal.js';
7
+ export { defaultPersistence, type SessionPersistence } from './persistence.js';
8
+ export { WsCore, type WsCoreOptions, type WSStatus, type ReconnectOptions, type WebSocketFactory, type WebSocketLike, } from './ws-core.js';
9
+ export type { Seq, OutputEvent, ReadyEvent, ExitEvent, CreateSessionResponse, } from '../shared/index.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACN,YAAY,EACZ,aAAa,EACb,KAAK,mBAAmB,EACxB,KAAK,WAAW,GAChB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5D,OAAO,EACN,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,cAAc,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC/E,OAAO,EACN,MAAM,EACN,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,aAAa,GAClB,MAAM,cAAc,CAAC;AAGtB,YAAY,EACX,GAAG,EACH,WAAW,EACX,UAAU,EACV,SAAS,EACT,qBAAqB,GACrB,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `ptykit/client` — framework-agnostic browser client.
3
+ */
4
+ export { PtyKitClient, ClientSession, } from './pty-kit-client.js';
5
+ export { attachFit } from './fit.js';
6
+ export { mountTerminal, } from './terminal.js';
7
+ export { defaultPersistence } from './persistence.js';
8
+ export { WsCore, } from './ws-core.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACN,YAAY,EACZ,aAAa,GAGb,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAyB,MAAM,UAAU,CAAC;AAC5D,OAAO,EACN,aAAa,GAGb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,kBAAkB,EAA2B,MAAM,kBAAkB,CAAC;AAC/E,OAAO,EACN,MAAM,GAMN,MAAM,cAAc,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * sessionId persistence (R13, Recommendation 2).
3
+ *
4
+ * Default: the active session id is stored in `sessionStorage`, scoped per
5
+ * namespace, with an in-memory fallback when storage is unavailable (SSR / Node).
6
+ * Callers that own the authoritative tab list (e.g. a server DB) override via the
7
+ * `persistence` hook.
8
+ */
9
+ export interface SessionPersistence {
10
+ load(namespace: string): string | null;
11
+ save(namespace: string, sessionId: string): void;
12
+ }
13
+ /** The default sessionStorage-backed persistence (with in-memory fallback). */
14
+ export declare function defaultPersistence(): SessionPersistence;
15
+ //# sourceMappingURL=persistence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../src/client/persistence.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,kBAAkB;IAClC,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACvC,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACjD;AAID,+EAA+E;AAC/E,wBAAgB,kBAAkB,IAAI,kBAAkB,CAiCvD"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * sessionId persistence (R13, Recommendation 2).
3
+ *
4
+ * Default: the active session id is stored in `sessionStorage`, scoped per
5
+ * namespace, with an in-memory fallback when storage is unavailable (SSR / Node).
6
+ * Callers that own the authoritative tab list (e.g. a server DB) override via the
7
+ * `persistence` hook.
8
+ */
9
+ const storageKey = (namespace) => `ptykit-active-session-${namespace}`;
10
+ /** The default sessionStorage-backed persistence (with in-memory fallback). */
11
+ export function defaultPersistence() {
12
+ const hasStorage = (() => {
13
+ try {
14
+ return typeof sessionStorage !== 'undefined' && sessionStorage !== null;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ })();
20
+ const memory = new Map();
21
+ return {
22
+ load(namespace) {
23
+ if (hasStorage) {
24
+ try {
25
+ return sessionStorage.getItem(storageKey(namespace));
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ return memory.get(namespace) ?? null;
32
+ },
33
+ save(namespace, sessionId) {
34
+ if (hasStorage) {
35
+ try {
36
+ sessionStorage.setItem(storageKey(namespace), sessionId);
37
+ return;
38
+ }
39
+ catch {
40
+ /* fall through to memory */
41
+ }
42
+ }
43
+ memory.set(namespace, sessionId);
44
+ },
45
+ };
46
+ }
47
+ //# sourceMappingURL=persistence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persistence.js","sourceRoot":"","sources":["../../src/client/persistence.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,MAAM,UAAU,GAAG,CAAC,SAAiB,EAAE,EAAE,CAAC,yBAAyB,SAAS,EAAE,CAAC;AAE/E,+EAA+E;AAC/E,MAAM,UAAU,kBAAkB;IACjC,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE;QACxB,IAAI,CAAC;YACJ,OAAO,OAAO,cAAc,KAAK,WAAW,IAAI,cAAc,KAAK,IAAI,CAAC;QACzE,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,KAAK,CAAC;QACd,CAAC;IACF,CAAC,CAAC,EAAE,CAAC;IACL,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEzC,OAAO;QACN,IAAI,CAAC,SAAS;YACb,IAAI,UAAU,EAAE,CAAC;gBAChB,IAAI,CAAC;oBACJ,OAAO,cAAc,CAAC,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;gBACtD,CAAC;gBAAC,MAAM,CAAC;oBACR,OAAO,IAAI,CAAC;gBACb,CAAC;YACF,CAAC;YACD,OAAO,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC;QACtC,CAAC;QACD,IAAI,CAAC,SAAS,EAAE,SAAS;YACxB,IAAI,UAAU,EAAE,CAAC;gBAChB,IAAI,CAAC;oBACJ,cAAc,CAAC,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC;oBACzD,OAAO;gBACR,CAAC;gBAAC,MAAM,CAAC;oBACR,4BAA4B;gBAC7B,CAAC;YACF,CAAC;YACD,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAClC,CAAC;KACD,CAAC;AACH,CAAC"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `PtyKitClient` — the browser client.
3
+ *
4
+ * One `WsCore` socket multiplexes every session in a namespace. Server output
5
+ * broadcasts to the room; this client filters by `sessionId` and dedups by
6
+ * `seq` (R5). Replayed frames (no `seq`) are passed through `stripReportRequests`
7
+ * so the terminal does not answer color/cursor queries into an idle prompt (R17).
8
+ *
9
+ * Reconnect is ON by default; on reconnect every known session is re-attached
10
+ * (idempotent `create-session`) so the room subscription and scrollback recover
11
+ * (R7/R14).
12
+ */
13
+ import { type Seq } from '../shared/index.js';
14
+ import { type ReconnectOptions, type WSStatus, type WebSocketFactory } from './ws-core.js';
15
+ import { type SessionPersistence } from './persistence.js';
16
+ export interface PtyKitClientOptions {
17
+ url: string;
18
+ /** Default namespace for `create`/`attach` when not passed explicitly. */
19
+ namespace?: string;
20
+ reconnect?: ReconnectOptions;
21
+ persistence?: SessionPersistence;
22
+ /** Injectable WebSocket constructor (defaults to global `WebSocket`). */
23
+ WebSocketImpl?: WebSocketFactory;
24
+ requestTimeoutMs?: number;
25
+ }
26
+ export interface OpenOptions {
27
+ sessionId?: string;
28
+ namespace?: string;
29
+ cwd?: string;
30
+ cols?: number;
31
+ rows?: number;
32
+ shell?: string;
33
+ }
34
+ interface CreateRequest {
35
+ sessionId: string;
36
+ namespace: string;
37
+ cwd?: string;
38
+ cols?: number;
39
+ rows?: number;
40
+ shell?: string;
41
+ }
42
+ type DataCb = (chunk: string) => void;
43
+ type ExitCb = (exitCode: number) => void;
44
+ type DirCb = (directory: string) => void;
45
+ type ErrCb = (error: string) => void;
46
+ /** A handle to one PTY session over the shared socket. */
47
+ export declare class ClientSession {
48
+ private readonly client;
49
+ readonly sessionId: string;
50
+ readonly namespace: string;
51
+ /** @internal — the request used to (re)attach on reconnect. */
52
+ readonly createRequest: CreateRequest;
53
+ private lastSeq;
54
+ /** Output that arrived before the first `onData` listener (e.g. reattach replay). */
55
+ private pending;
56
+ private pendingBytes;
57
+ private static readonly PENDING_CAP;
58
+ private readonly dataCbs;
59
+ private readonly exitCbs;
60
+ private readonly dirCbs;
61
+ private readonly errCbs;
62
+ constructor(client: PtyKitClient, request: CreateRequest);
63
+ /** Subscribe to terminal output (deduped by seq; replay frames stripped). */
64
+ onData(cb: DataCb): () => void;
65
+ onExit(cb: ExitCb): () => void;
66
+ onDirectory(cb: DirCb): () => void;
67
+ onError(cb: ErrCb): () => void;
68
+ /** @internal Route an output event for this session. */
69
+ _handleOutput(content: string, seq?: Seq): void;
70
+ /** @internal */
71
+ _handleExit(exitCode: number): void;
72
+ /** @internal */
73
+ _handleDirectory(directory: string): void;
74
+ /** @internal */
75
+ _handleError(error: string): void;
76
+ /** Send raw keystrokes (fire-and-forget pass-through, R16). */
77
+ write(data: string): void;
78
+ resize(cols: number, rows: number): Promise<void>;
79
+ cancel(): Promise<void>;
80
+ clear(): Promise<void>;
81
+ /** Kill the session on the server. */
82
+ kill(): Promise<void>;
83
+ /** Stop receiving locally; does NOT kill the server-side session. */
84
+ detach(): void;
85
+ }
86
+ export declare class PtyKitClient {
87
+ private readonly core;
88
+ private readonly persistence;
89
+ private readonly defaultNamespace?;
90
+ private readonly sessions;
91
+ private status;
92
+ private readonly statusCbs;
93
+ constructor(options: PtyKitClientOptions);
94
+ /** Subscribe to connection status. Fires immediately with the current value. */
95
+ onStatus(cb: (status: WSStatus) => void): () => void;
96
+ connected(): boolean;
97
+ private resolveNamespace;
98
+ private generateId;
99
+ /** Create a new session (or attach if the id already exists — server is idempotent). */
100
+ create(options?: OpenOptions): Promise<ClientSession>;
101
+ /** Attach to an existing session (replays serialized scrollback). */
102
+ attach(sessionId?: string, options?: OpenOptions): Promise<ClientSession>;
103
+ private open;
104
+ private reattachAll;
105
+ /** List sessions a namespace currently has on the server. */
106
+ listSessions(namespace?: string): Promise<any>;
107
+ disconnect(): void;
108
+ /** @internal */
109
+ _emitInput(sessionId: string, data: string): void;
110
+ /** @internal */
111
+ _resize(sessionId: string, cols: number, rows: number): Promise<void>;
112
+ /** @internal */
113
+ _cancel(sessionId: string): Promise<void>;
114
+ /** @internal */
115
+ _clear(sessionId: string): Promise<void>;
116
+ /** @internal */
117
+ _kill(sessionId: string): Promise<void>;
118
+ /** @internal */
119
+ _forget(sessionId: string): void;
120
+ }
121
+ export {};
122
+ //# sourceMappingURL=pty-kit-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pty-kit-client.d.ts","sourceRoot":"","sources":["../../src/client/pty-kit-client.ts"],"names":[],"mappings":"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"}