@getorrery/client 0.0.0-stage → 0.2.2

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.
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The WebSocket transport: the pipe listener's protocol, one frame per
3
+ * message (platform plan 02, "WebSocket listener: one frame per message,
4
+ * unchanged").
5
+ *
6
+ * The pipe frames each body with a `u32` length (`orrery-transport/src/frame.rs`);
7
+ * a WebSocket message already has a length, so the body *is* the message:
8
+ *
9
+ * 1. client → server: the [`Hello`], JSON text;
10
+ * 2. server → client: the [`HelloAck`], JSON text;
11
+ * 3. server → client: one AG-UI frame per message, replay first;
12
+ * 4. client → server: one `orrery_proto::Request` per message.
13
+ *
14
+ * Only JSON: CBOR is negotiable on the pipe, but a browser has no CBOR codec
15
+ * and the relay forwards bytes it never decodes (platform invariant 5).
16
+ *
17
+ * Every request is answered by a reply message (harness decision 0045):
18
+ * `{"t":"reply","reqId":"<request id>","ok":true,"value":…}` or
19
+ * `{"t":"reply","reqId":"…","ok":false,"error":"what to do"}`. Replies carry
20
+ * no `seq`; frames always have `seq` and never `t`. `send` resolves with the
21
+ * `value` or rejects with the `error`, matched on `request.id` == `reqId`;
22
+ * it rejects on close, and after 30 s with no reply.
23
+ *
24
+ * Browser-safe. In Node ≥ 22 the global `WebSocket` works; any other
25
+ * implementation (the `ws` package) can be passed as `WebSocket`.
26
+ */
27
+ import { type CloseListener, type FrameListener, type Hello, type HelloAck, type Request, type Transport, type Unsubscribe } from "./transport.js";
28
+ /** The part of the WHATWG `WebSocket` this transport uses. */
29
+ export interface WebSocketLike {
30
+ readonly readyState: number;
31
+ binaryType: string;
32
+ send(data: string): void;
33
+ close(code?: number, reason?: string): void;
34
+ addEventListener(type: "open", listener: () => void): void;
35
+ addEventListener(type: "message", listener: (event: {
36
+ data: unknown;
37
+ }) => void): void;
38
+ addEventListener(type: "close", listener: (event: {
39
+ code: number;
40
+ reason: string;
41
+ }) => void): void;
42
+ addEventListener(type: "error", listener: (event: unknown) => void): void;
43
+ }
44
+ /** A `WebSocket` constructor: the global one, or `ws`'s. */
45
+ export type WebSocketCtor = new (url: string, protocols?: string | string[]) => WebSocketLike;
46
+ export interface WebSocketTransportOptions {
47
+ /** `ws(s)://host:port/path`. */
48
+ url: string;
49
+ /** Sent in the [`Hello`]: a browser cannot put a header on a WebSocket. */
50
+ token?: string;
51
+ /** The implementation; the global `WebSocket` when absent. */
52
+ WebSocket?: WebSocketCtor;
53
+ }
54
+ export declare class WebSocketTransport implements Transport {
55
+ readonly kind = "ws";
56
+ private readonly url;
57
+ private readonly token;
58
+ private readonly Ctor;
59
+ private socket;
60
+ private hub;
61
+ private readonly pending;
62
+ constructor(options: WebSocketTransportOptions);
63
+ connect(hello: Hello): Promise<HelloAck>;
64
+ subscribe(onFrame: FrameListener, onClose?: CloseListener): Unsubscribe;
65
+ send(request: Request): Promise<unknown>;
66
+ private failPending;
67
+ close(): Promise<void>;
68
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * How a client reaches a session: a pluggable transport (platform plan 04).
3
+ *
4
+ * Every transport carries the same three things the pipe listener carries
5
+ * (`orrery-transport/src/listener/pipe.rs` + `frame.rs`):
6
+ *
7
+ * 1. a [`Hello`] out and a [`HelloAck`] back — always JSON, always first;
8
+ * 2. AG-UI [`Frame`]s in, each with its `seq`;
9
+ * 3. control [`Request`]s out.
10
+ *
11
+ * Two implementations ship: [`HttpSseTransport`] (`/events` as SSE, `/control`
12
+ * as JSON, what `orrery serve` speaks today) and [`WebSocketTransport`] (one
13
+ * frame per message, what the relay and the node's WebSocket listener speak).
14
+ * Neither imports anything from `node:`; this file is browser-safe.
15
+ */
16
+ import { type Frame } from "./events.js";
17
+ /** The AG-UI protocol version this client was built against (`orrery_agui::AGUI_PROTOCOL_VERSION`). */
18
+ export declare const AGUI_PROTOCOL_VERSION = "1.0";
19
+ /** A control request, as `orrery-proto` spells it. */
20
+ export type Request = Record<string, unknown> & {
21
+ t: string;
22
+ id: string;
23
+ };
24
+ /**
25
+ * The first message on a connection. Always JSON.
26
+ *
27
+ * `format`, `protocol`, `session` and `since` are `orrery_transport::frame::Hello`
28
+ * verbatim. `machine`, `token`, `client` and `draws` are this SDK's additions
29
+ * for the relay and for a browser (which cannot set an `Authorization` header
30
+ * on a WebSocket); a server that does not know them ignores them, as serde
31
+ * does for any unknown field.
32
+ */
33
+ export interface Hello {
34
+ format: "json";
35
+ protocol: string;
36
+ /** The session to attach to. */
37
+ session?: string;
38
+ /** The machine the session runs on, when it goes through the relay (`[machine/]session`). */
39
+ machine?: string;
40
+ /** Replay from just after this. Absent means from the start. */
41
+ since?: number;
42
+ /** A bearer token, for transports that cannot carry a header. */
43
+ token?: string;
44
+ /** What this client is, and what it draws (decision 0041 §2, 0042). */
45
+ client?: string;
46
+ draws?: string[];
47
+ }
48
+ /** What a server says back to a [`Hello`] (`orrery_transport::listener::pipe::HelloAck`). */
49
+ export interface HelloAck {
50
+ format: "json";
51
+ protocol: string;
52
+ /** The last `seq` the session has reached. */
53
+ last_seq?: number;
54
+ /** Set when `since` was older than the replay ring: ask the session store. */
55
+ replay_too_old?: number;
56
+ /**
57
+ * This connection's number (harness decision 0058): a flow's question whose
58
+ * `requester.conn` is another is somebody else's. HTTP reads it from the
59
+ * `x-orrery-conn` header of `GET /events`.
60
+ */
61
+ conn?: number;
62
+ }
63
+ /**
64
+ * A control request the server refused. `message` is the server's words (what
65
+ * to do); `code` is the stable reason when the request has one
66
+ * (`workspace.*`, harness decision 0051): the reply's `code` on a WebSocket,
67
+ * the `x-orrery-code` header over HTTP.
68
+ */
69
+ export declare class ControlError extends Error {
70
+ readonly code: string | undefined;
71
+ constructor(message: string, code?: string);
72
+ }
73
+ /** Called with every frame, in order. */
74
+ export type FrameListener = (frame: Frame) => void;
75
+ /** Called once when the stream ends: `undefined` for a clean close, the error otherwise. */
76
+ export type CloseListener = (error?: Error) => void;
77
+ /** Stops a subscription. */
78
+ export type Unsubscribe = () => void;
79
+ /**
80
+ * One way to reach a session. Every client draws through one of these; none
81
+ * knows which.
82
+ *
83
+ * The contract, checked by `test/transport.test.ts` against both
84
+ * implementations:
85
+ *
86
+ * - `connect` sends the [`Hello`] and resolves with the [`HelloAck`]. Frames
87
+ * replayed from `since` arrive after it, through `subscribe`.
88
+ * - `subscribe` may be called before or after `connect`; frames that arrive
89
+ * with no subscriber are buffered and handed to the first one, so none is
90
+ * lost between the two calls.
91
+ * - `send` delivers one [`Request`]. It resolves with the server's answer
92
+ * (HTTP: the `/control` body; WebSocket: the `value` of the `reply` message
93
+ * whose `reqId` is `request.id`, harness decision 0045) and rejects with the
94
+ * server's error text when it refuses.
95
+ * - `close` ends the stream; every close listener is told once.
96
+ */
97
+ export interface Transport {
98
+ /** What this transport is called in messages: `http`, `ws`. */
99
+ readonly kind: string;
100
+ connect(hello: Hello): Promise<HelloAck>;
101
+ send(request: Request): Promise<unknown>;
102
+ subscribe(onFrame: FrameListener, onClose?: CloseListener): Unsubscribe;
103
+ close(): Promise<void>;
104
+ }
105
+ /**
106
+ * The frames of a transport as an async iterable: ends when the transport
107
+ * closes, throws when it fails.
108
+ */
109
+ export declare function framesOf(transport: Transport): AsyncIterable<Frame>;
110
+ /**
111
+ * Fan-out with a buffer: frames that arrive before anyone subscribes are kept
112
+ * for the first subscriber. Shared by both transports.
113
+ */
114
+ export declare class FrameHub {
115
+ private readonly frames;
116
+ private readonly closes;
117
+ private pending;
118
+ private ended;
119
+ subscribe(onFrame: FrameListener, onClose?: CloseListener): Unsubscribe;
120
+ push(frame: Frame): void;
121
+ end(error?: Error): void;
122
+ get isEnded(): boolean;
123
+ }
124
+ /**
125
+ * Where a session lives: `[machine/]session`.
126
+ *
127
+ * A bare `session` is on the machine the transport reaches directly (the local
128
+ * node, or the node a URL names). `machine/session` goes through the relay,
129
+ * which routes by the pair (platform architecture, "A client attaches").
130
+ */
131
+ export interface SessionAddress {
132
+ machine: string | null;
133
+ session: string;
134
+ }
135
+ /** Parse `[machine/]session`. Throws, saying the form, on anything else. */
136
+ export declare function parseAddress(raw: string): SessionAddress;
137
+ /** The address as text: `machine/session`, or `session`. */
138
+ export declare function formatAddress(a: SessionAddress): string;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The person's own actions on a session's workspace (harness decision 0051):
3
+ * read a file, commit, push. Node-level requests, answered by the node that
4
+ * holds the session, on every transport and through the relay, like
5
+ * `sessions.list`. Not agent tools: no consent prompt; the node audits each
6
+ * as the person's.
7
+ *
8
+ * ```ts
9
+ * const file = await workspaceRead(transport, { session, path: "src/main.rs" });
10
+ * const { sha } = await workspaceCommit(transport, { session, message: "fix the build" });
11
+ * const { ok, output } = await workspacePush(transport, { session });
12
+ * ```
13
+ *
14
+ * A refusal rejects with a {@link ControlError} whose `code` is a
15
+ * {@link WorkspaceErrorCode} (`too_large`, `outside_workspace`,
16
+ * `nothing_to_commit`, `no_remote`, …) and whose message says the fix.
17
+ *
18
+ * A client with a control channel of its own (the ADE's IPC) builds the
19
+ * request with {@link workspaceRequest} and reads the answer as the types
20
+ * below.
21
+ */
22
+ import type { WorkspaceCommitted, WorkspaceErrorCode, WorkspaceFile, WorkspacePushed } from "@getorrery/protocol";
23
+ import { type Request, type Transport } from "./transport.js";
24
+ export type { WorkspaceCommitted, WorkspaceErrorCode, WorkspaceFile, WorkspacePushed } from "@getorrery/protocol";
25
+ /** The `t` of each request. */
26
+ export declare const WORKSPACE_READ = "workspace.read";
27
+ export declare const WORKSPACE_COMMIT = "workspace.commit";
28
+ export declare const WORKSPACE_PUSH = "workspace.push";
29
+ /** The largest file `workspace.read` answers with (`orrery_proto::workspace::READ_CAP_BYTES`). */
30
+ export declare const WORKSPACE_READ_CAP_BYTES: number;
31
+ /** `workspace.read`: one file's text, relative to the session's workspace. */
32
+ export interface WorkspaceReadAsk {
33
+ session: string;
34
+ path: string;
35
+ /**
36
+ * The file as git's index holds it, not the disk: the side the live changes
37
+ * are measured against (harness decision 0056). Not in the index (a new
38
+ * file): refused with `not_found`.
39
+ */
40
+ base?: boolean;
41
+ }
42
+ /** `workspace.commit`: every change, or only `paths`. */
43
+ export interface WorkspaceCommitAsk {
44
+ session: string;
45
+ message: string;
46
+ /** Only these paths, relative to the workspace. Absent: every change. */
47
+ paths?: string[];
48
+ }
49
+ /** `workspace.push`: the session's branch to its remote. */
50
+ export interface WorkspacePushAsk {
51
+ session: string;
52
+ }
53
+ /** The wire form of each request, with a fresh `id`. */
54
+ export declare const workspaceRequest: {
55
+ read(ask: WorkspaceReadAsk): Request;
56
+ commit(ask: WorkspaceCommitAsk): Request;
57
+ push(ask: WorkspacePushAsk): Request;
58
+ };
59
+ /** Read one file of the session's workspace as text. */
60
+ export declare function workspaceRead(transport: Transport, ask: WorkspaceReadAsk): Promise<WorkspaceFile>;
61
+ /** Commit the session's workspace; resolves with the new commit's sha. */
62
+ export declare function workspaceCommit(transport: Transport, ask: WorkspaceCommitAsk): Promise<WorkspaceCommitted>;
63
+ /** Push the session's branch; `ok: false` with git's words when the remote refused it. */
64
+ export declare function workspacePush(transport: Transport, ask: WorkspacePushAsk): Promise<WorkspacePushed>;
65
+ /** The workspace refusal's code, when `error` is one. */
66
+ export declare function workspaceErrorCode(error: unknown): WorkspaceErrorCode | undefined;
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "@getorrery/client",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.2",
4
+ "description": "The TypeScript Orrery client SDK: an AG-UI session over a pluggable transport (HTTP+SSE, WebSocket) and a SurfaceStore. Pure data, no drawing. Browser-safe; Node-only parts in @getorrery/client/node.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "node": "./dist/node.js",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./node": {
15
+ "types": "./dist/node.d.ts",
16
+ "default": "./dist/node.js"
17
+ },
18
+ "./cloud-events": {
19
+ "types": "./dist/cloud-events.d.ts",
20
+ "default": "./dist/cloud-events.js"
21
+ },
22
+ "./browser": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/kouji-dev/orrery",
31
+ "directory": "harness/clients/sdk-ts"
32
+ },
33
+ "dependencies": {
34
+ "@ag-ui/client": "^1.0.0",
35
+ "@ag-ui/core": "^1.0.0",
36
+ "@getorrery/ext": "0.2.2",
37
+ "@getorrery/protocol": "0.2.2"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^22.10.0",
41
+ "@types/ws": "^8.18.2",
42
+ "typescript": "^5.9.3",
43
+ "vitest": "^4.1.8",
44
+ "ws": "^8.21.3"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "files": [
50
+ "dist",
51
+ "README.md"
52
+ ],
53
+ "scripts": {
54
+ "build": "node ../../../scripts/release/build-lib.mjs src/index.ts src/node.ts src/cloud-events.ts",
55
+ "typecheck": "tsc --noEmit",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest"
58
+ }
6
59
  }