Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 71 additions & 11 deletions cmux-tui/bindings/typescript/README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,95 @@
# cmux TypeScript Client

Node.js client for the cmux-tui Unix-socket JSON-lines protocol.
The typed client library for cmux-tui frontends. It exposes every implemented
command and event in protocol v6, transport-independent request handling,
browser-safe attach streams, and Node.js Unix-socket defaults.

## Build
## Install and build

```bash
npm i cmux
npm install
npm install cmux
npm run build
```

The package has no runtime dependencies. Node 20 or newer is required.
The package has no runtime dependencies. The Node entry requires Node 20 or
newer. Browser bundles resolve the root `browser` export condition, and the
same browser-safe surface is available explicitly from `cmux/browser`.

## Usage
## Building a frontend

This example uses an existing xterm.js terminal and a cmux WebSocket endpoint.
Attach payloads are decoded to `Uint8Array`, which xterm.js accepts directly.

```ts
import { Terminal } from "@xterm/xterm";
import { CmuxClient, WebSocketTransport } from "cmux";
const terminal = new Terminal();
const transport = new WebSocketTransport("ws://127.0.0.1:9000/api/v1/ws");
const client = new CmuxClient({ transport });
const info = await client.identify();
console.log(`cmux protocol ${info.protocol}`);
const tree = await client.listWorkspaces();
const workspace = tree.workspaces.find(({ active }) => active);
const screen = workspace?.screens.find(({ active }) => active);
const pane = screen?.panes.find((item) => "tabs" in item);
const surface = pane && "tabs" in pane ? pane.tabs[pane.active_tab]?.surface : undefined;
if (surface === undefined) throw new Error("No active surface");
const stream = await client.attachSurface(surface);
void (async () => {
for await (const event of stream) {
if (event.event === "vt-state" || event.event === "output") terminal.write(event.data);
}
})();
await client.send(surface, { bytes: new TextEncoder().encode("ls\r") });
```

`WebSocketTransport` uses the browser's global `WebSocket`. In Node, inject any
compatible constructor without adding a runtime dependency to this package:

```ts
import WebSocket from "ws";
import { CmuxClient, WebSocketTransport } from "cmux";

const client = new CmuxClient({
transport: new WebSocketTransport("ws://127.0.0.1:9000/api/v1/ws", WebSocket),
});
```

## Node Unix socket

The default Node entry preserves the original zero-argument API:

```ts
import { CmuxClient } from "cmux";
import { CmuxClient } from "cmux/node";

const client = new CmuxClient();
const info = await client.identify();
const created = await client.newWorkspace({ name: "sdk-demo", cols: 80, rows: 24 });
await client.send(created.surface, { text: "echo hello\r" });
console.log((await client.readScreen(created.surface)).text);
await client.close();
```

`new CmuxClient()` uses `CMUX_TUI_SOCKET` when set, then legacy
`CMUX_MUX_SOCKET`, then the default session socket path.
`new CmuxClient()` uses `CMUX_TUI_SOCKET`, then legacy `CMUX_MUX_SOCKET`, then
the default session socket. Unix subscribe and attach streams retain dedicated
connections; an injected transport can multiplex them on its main connection.

## Raw typed requests

Every command method delegates to the same generic escape hatch:

```ts
const result = await client.request({ cmd: "copy", surface: 1, mode: "screen" });
```

The `cmd` discriminator determines both required parameters and the successful
response data type. `sendRaw()` remains available for untyped forward
compatibility.

## E2E
## Verification

```bash
npm ci
npm run build
npm test
CMUX_TUI_SOCKET=/path/to/session.sock npm run e2e
```
37 changes: 33 additions & 4 deletions cmux-tui/bindings/typescript/package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
{
"name": "cmux",
"version": "0.1.2",
"description": "Node.js client for the cmux-tui JSON-lines protocol",
"description": "Typed TypeScript client for cmux-tui frontends",
"repository": {
"type": "git",
"url": "git+https://github.com/manaflow-ai/cmux.git",
"directory": "cmux-tui/bindings/typescript"
},
"homepage": "https://github.com/manaflow-ai/cmux/tree/main/cmux-tui/bindings/typescript",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"type": "module",
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"exports": {
".": {
"browser": {
"types": "./dist/src/browser.d.ts",
"import": "./dist/src/browser.js",
"default": "./dist/src/browser.js"
},
"types": "./dist/src/index.d.ts",
"import": "./dist/src/index.js",
"default": "./dist/src/index.js"
},
"./browser": {
"types": "./dist/src/browser.d.ts",
"import": "./dist/src/browser.js",
"default": "./dist/src/browser.js"
},
"./node": {
"types": "./dist/src/index.d.ts",
"import": "./dist/src/index.js",
"default": "./dist/src/index.js"
}
},
"sideEffects": false,
"files": [
"dist/src",
"README.md"
],
"scripts": {
"build": "tsc",
"e2e": "tsc && node dist/e2e/e2e.js"
"test": "npm run build && node --test dist/test/*.test.js",
"e2e": "npm run build && node dist/e2e/e2e.js"
},
"engines": {
"node": ">=20"
Expand Down
48 changes: 48 additions & 0 deletions cmux-tui/bindings/typescript/src/base64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
interface NodeBufferValue {
readonly length: number;
readonly [index: number]: number;
toString(encoding: "base64"): string;
}

interface NodeBufferConstructor {
from(value: Uint8Array): NodeBufferValue;
from(value: string, encoding: "base64"): NodeBufferValue;
}

function nodeBuffer(): NodeBufferConstructor | undefined {
return (globalThis as typeof globalThis & { Buffer?: NodeBufferConstructor }).Buffer;
}

/** Decodes standard base64 without requiring Node's `Buffer`. */
export function decodeBase64(value: string): Uint8Array {
if (typeof globalThis.atob === "function") {
const decoded = globalThis.atob(value);
const bytes = new Uint8Array(decoded.length);
for (let index = 0; index < decoded.length; index += 1) bytes[index] = decoded.charCodeAt(index);
return bytes;
}

const Buffer = nodeBuffer();
if (!Buffer) throw new Error("base64 decoding is not available in this runtime");
const decoded = Buffer.from(value, "base64");
const bytes = new Uint8Array(decoded.length);
for (let index = 0; index < decoded.length; index += 1) bytes[index] = decoded[index];
return bytes;
}

/** Encodes bytes as standard base64 without requiring Node's `Buffer`. */
export function encodeBase64(value: Uint8Array): string {
if (typeof globalThis.btoa === "function") {
let binary = "";
const chunkSize = 0x8000;
for (let offset = 0; offset < value.length; offset += chunkSize) {
const chunk = value.subarray(offset, Math.min(offset + chunkSize, value.length));
for (const byte of chunk) binary += String.fromCharCode(byte);
}
return globalThis.btoa(binary);
}

const Buffer = nodeBuffer();
if (!Buffer) throw new Error("base64 encoding is not available in this runtime");
return Buffer.from(value).toString("base64");
}
19 changes: 19 additions & 0 deletions cmux-tui/bindings/typescript/src/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export {
CmuxClient,
CmuxStream,
type CmuxClientOptions,
type CmuxClientOptions as ClientOptions,
type NewBrowserTabOptions,
type NewScreenOptions,
type NewTabOptions,
type NewWorkspaceOptions,
type SelectOptions,
type SelectTabOptions,
type SendOptions,
type SplitOptions,
} from "./client.js";
export * from "./base64.js";
export * from "./errors.js";
export * from "./protocol/index.js";
export * from "./transport.js";
export * from "./websocket-transport.js";
Loading
Loading