@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.
- package/README.md +38 -2
- package/dist/anim.d.ts +23 -0
- package/dist/channel.d.ts +24 -0
- package/dist/chunk-P3UGNLD6.js +135 -0
- package/dist/chunk-RQUTHT7N.js +1551 -0
- package/dist/cloud-events.d.ts +94 -0
- package/dist/cloud-events.js +14 -0
- package/dist/conformance-node.d.ts +11 -0
- package/dist/conformance.d.ts +58 -0
- package/dist/events.d.ts +173 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +112 -0
- package/dist/kept.d.ts +82 -0
- package/dist/node.d.ts +11 -0
- package/dist/node.js +175 -0
- package/dist/session.d.ts +210 -0
- package/dist/store.d.ts +277 -0
- package/dist/transport-http.d.ts +37 -0
- package/dist/transport-ws.d.ts +68 -0
- package/dist/transport.d.ts +138 -0
- package/dist/workspace.d.ts +66 -0
- package/package.json +56 -3
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
}
|