@neta-art/cohub 1.1.0 → 1.2.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.
- package/README.md +34 -14
- package/dist/apis/invitations.d.ts +20 -0
- package/dist/apis/invitations.js +36 -0
- package/dist/apis/models.d.ts +4 -5
- package/dist/apis/models.js +5 -13
- package/dist/apis/spaces.d.ts +60 -3
- package/dist/apis/spaces.js +91 -3
- package/dist/client.d.ts +2 -0
- package/dist/client.js +35 -5
- package/dist/environment.d.ts +22 -0
- package/dist/environment.js +37 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +8 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/realtime.js +3 -0
- package/dist/transport.d.ts +2 -0
- package/dist/transport.js +2 -1
- package/dist/types.d.ts +62 -0
- package/dist/websocket.d.ts +15 -1
- package/dist/websocket.js +60 -41
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -13,6 +13,39 @@ npm install @neta-art/cohub @neta-art/cohub-protocol
|
|
|
13
13
|
```ts
|
|
14
14
|
import { createCohubClient } from "@neta-art/cohub";
|
|
15
15
|
|
|
16
|
+
const client = createCohubClient({
|
|
17
|
+
getAccessToken: async () => localStorage.getItem("token"),
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The SDK connects to production by default:
|
|
22
|
+
|
|
23
|
+
- API: `https://api.cohub.run`
|
|
24
|
+
- WebSocket: `wss://gateway.cohub.run/ws`
|
|
25
|
+
|
|
26
|
+
Use development with `ENV=dev` in Node.js:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
ENV=dev node app.js
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or select it explicitly in code:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const client = createCohubClient({
|
|
36
|
+
env: "dev",
|
|
37
|
+
getAccessToken: async () => localStorage.getItem("token"),
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Development uses:
|
|
42
|
+
|
|
43
|
+
- API: `https://api-dev.cohub.run`
|
|
44
|
+
- WebSocket: `wss://gateway-dev.cohub.run/ws`
|
|
45
|
+
|
|
46
|
+
Custom endpoints are still supported when needed:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
16
49
|
const client = createCohubClient({
|
|
17
50
|
baseUrl: "https://api.example.com",
|
|
18
51
|
getAccessToken: async () => localStorage.getItem("token"),
|
|
@@ -87,7 +120,6 @@ If you only want HTTP transport, use the dedicated entry:
|
|
|
87
120
|
import { createHttpClient } from "@neta-art/cohub/http";
|
|
88
121
|
|
|
89
122
|
const http = createHttpClient({
|
|
90
|
-
baseUrl: "https://api.example.com",
|
|
91
123
|
getAccessToken: async () => localStorage.getItem("token"),
|
|
92
124
|
});
|
|
93
125
|
|
|
@@ -104,7 +136,6 @@ If you need direct realtime transport access, use the websocket entry:
|
|
|
104
136
|
import { createWebsocketClient } from "@neta-art/cohub/websocket";
|
|
105
137
|
|
|
106
138
|
const ws = createWebsocketClient({
|
|
107
|
-
url: "https://gateway.example.com",
|
|
108
139
|
getAccessToken: async () => localStorage.getItem("token"),
|
|
109
140
|
});
|
|
110
141
|
|
|
@@ -118,15 +149,4 @@ This SDK is intentionally built around Cohub's co-creation model:
|
|
|
118
149
|
- work with `space(...)` and `session(...)` as the primary creative surface
|
|
119
150
|
- send messages through `session.messages.send(...)`
|
|
120
151
|
- subscribe through `space.subscribe(...)` and `session.subscribe(...)`
|
|
121
|
-
- keep
|
|
122
|
-
|
|
123
|
-
## Publish checklist
|
|
124
|
-
|
|
125
|
-
Before publishing:
|
|
126
|
-
|
|
127
|
-
1. build the protocol package: `pnpm --filter @neta-art/cohub-protocol build`
|
|
128
|
-
2. build this package: `pnpm --filter @neta-art/cohub build`
|
|
129
|
-
3. typecheck the protocol package: `pnpm --filter @neta-art/cohub-protocol typecheck`
|
|
130
|
-
4. typecheck this package: `pnpm --filter @neta-art/cohub typecheck`
|
|
131
|
-
5. verify consuming apps still typecheck
|
|
132
|
-
6. verify `dist/` contains `index`, `http`, and `websocket` outputs
|
|
152
|
+
- keep HTTP and realtime transports separate but coordinated
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HttpTransport } from "../transport.js";
|
|
2
|
+
import type { SpaceInvitation, CreateInvitationInput, CreateInvitationResponse, InvitationDetail, AcceptInvitationResponse } from "../types.js";
|
|
3
|
+
export declare class SpaceInvitationsApi {
|
|
4
|
+
private readonly transport;
|
|
5
|
+
private readonly spaceId;
|
|
6
|
+
constructor(transport: HttpTransport, spaceId: string);
|
|
7
|
+
list(): Promise<{
|
|
8
|
+
items: SpaceInvitation[];
|
|
9
|
+
}>;
|
|
10
|
+
create(input?: CreateInvitationInput): Promise<CreateInvitationResponse>;
|
|
11
|
+
revoke(token: string): Promise<{
|
|
12
|
+
ok: true;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
export declare class PublicInviteApi {
|
|
16
|
+
private readonly transport;
|
|
17
|
+
constructor(transport: HttpTransport);
|
|
18
|
+
get(token: string): Promise<InvitationDetail>;
|
|
19
|
+
accept(token: string): Promise<AcceptInvitationResponse>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export class SpaceInvitationsApi {
|
|
2
|
+
transport;
|
|
3
|
+
spaceId;
|
|
4
|
+
constructor(transport, spaceId) {
|
|
5
|
+
this.transport = transport;
|
|
6
|
+
this.spaceId = spaceId;
|
|
7
|
+
}
|
|
8
|
+
list() {
|
|
9
|
+
return this.transport.request(`/api/spaces/${this.spaceId}/invitations`);
|
|
10
|
+
}
|
|
11
|
+
create(input) {
|
|
12
|
+
return this.transport.request(`/api/spaces/${this.spaceId}/invitations`, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: { "Content-Type": "application/json" },
|
|
15
|
+
body: JSON.stringify(input ?? {}),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
revoke(token) {
|
|
19
|
+
return this.transport.request(`/api/spaces/${this.spaceId}/invitations/${token}`, { method: "DELETE" });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// Public invite API (no auth required for viewing)
|
|
23
|
+
export class PublicInviteApi {
|
|
24
|
+
transport;
|
|
25
|
+
constructor(transport) {
|
|
26
|
+
this.transport = transport;
|
|
27
|
+
}
|
|
28
|
+
get(token) {
|
|
29
|
+
return this.transport.request(`/api/invite/${token}`);
|
|
30
|
+
}
|
|
31
|
+
accept(token) {
|
|
32
|
+
return this.transport.request(`/api/invite/${token}/accept`, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
package/dist/apis/models.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HttpTransport } from "../transport.js";
|
|
2
2
|
import type { ModelCatalogEntry } from "../types.js";
|
|
3
3
|
export declare class ModelsApi {
|
|
4
|
-
private readonly
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
list(customFetch?: Fetch): Promise<Record<string, ModelCatalogEntry[]>>;
|
|
4
|
+
private readonly transport;
|
|
5
|
+
constructor(transport: HttpTransport);
|
|
6
|
+
list(): Promise<Record<string, ModelCatalogEntry[]>>;
|
|
8
7
|
}
|
package/dist/apis/models.js
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
export class ModelsApi {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
this.fetcher = fetcher;
|
|
6
|
-
this.baseUrl = baseUrl;
|
|
2
|
+
transport;
|
|
3
|
+
constructor(transport) {
|
|
4
|
+
this.transport = transport;
|
|
7
5
|
}
|
|
8
|
-
async list(
|
|
9
|
-
|
|
10
|
-
const url = this.baseUrl ? `${this.baseUrl}/api/models` : "/api/models";
|
|
11
|
-
const response = await fetchImpl(url);
|
|
12
|
-
if (!response.ok) {
|
|
13
|
-
throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
|
|
14
|
-
}
|
|
15
|
-
return response.json();
|
|
6
|
+
async list() {
|
|
7
|
+
return this.transport.request("/api/models");
|
|
16
8
|
}
|
|
17
9
|
}
|
package/dist/apis/spaces.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { WebsocketClient, WebsocketEventPayload } from "../websocket.js";
|
|
2
2
|
import type { HttpTransport, Fetch } from "../transport.js";
|
|
3
|
-
import type { CheckpointRecord, ContentBlock, SessionMessagesPaginatedResponse, SessionMessagesResponse, SessionRecord, SpaceAccessPolicy, SpaceBootstrapSource, SpaceChannelBindingInput, SpaceCheckpointDetailResponse, SpaceCreateResponse, SpaceEnvInput, SpaceFsFileResponse, SpaceFsMoveInput, SpaceFsTreeResponse, SpaceUsageResponse, SpaceFsWriteFileInput, SpaceMember, SpaceRecord, SpaceRole, SpaceSessionsResponse } from "../types.js";
|
|
3
|
+
import type { CheckpointRecord, ContentBlock, SessionMessageResponse, SessionMessagesPaginatedResponse, SessionMessagesResponse, SessionRecord, SpaceAccessPolicy, SpaceBootstrapSource, SpaceChannelBindingInput, SpaceCheckpointDetailResponse, SpaceCreateResponse, SpaceEnvInput, SpaceFsFileResponse, SpaceFsMoveInput, SpaceFsTreeResponse, SpaceFsUploadResponse, SpaceUsageResponse, SpaceFsWriteFileInput, SpaceMember, SpaceRecord, SpaceRole, SpaceSessionsResponse } from "../types.js";
|
|
4
|
+
import { SpaceInvitationsApi } from "./invitations.js";
|
|
4
5
|
export type SessionSubscriptionHandlers = {
|
|
5
6
|
progress?: (event: WebsocketEventPayload) => void;
|
|
6
7
|
final?: (event: WebsocketEventPayload) => void;
|
|
@@ -58,6 +59,7 @@ export declare class SpaceFilesApi {
|
|
|
58
59
|
fromPath: string;
|
|
59
60
|
toPath: string;
|
|
60
61
|
}>;
|
|
62
|
+
upload(files: File[], dir?: string): Promise<SpaceFsUploadResponse>;
|
|
61
63
|
}
|
|
62
64
|
declare class SessionMessagesClient {
|
|
63
65
|
private readonly transport;
|
|
@@ -67,6 +69,9 @@ declare class SessionMessagesClient {
|
|
|
67
69
|
private lastSentAt;
|
|
68
70
|
constructor(transport: HttpTransport, sessionId: string);
|
|
69
71
|
list(customFetch?: Fetch): Promise<SessionMessagesResponse>;
|
|
72
|
+
get(messageId: string, optionsOrFetch?: {
|
|
73
|
+
detail?: "summary" | "full";
|
|
74
|
+
} | Fetch, customFetch?: Fetch): Promise<SessionMessageResponse>;
|
|
70
75
|
listPaginated(options?: {
|
|
71
76
|
cursor?: number;
|
|
72
77
|
limit?: number;
|
|
@@ -114,13 +119,19 @@ export declare class SpaceSessionsApi {
|
|
|
114
119
|
ok: true;
|
|
115
120
|
session: SessionRecord;
|
|
116
121
|
}>;
|
|
117
|
-
list(
|
|
122
|
+
list(optionsOrFetch?: {
|
|
123
|
+
limit?: number;
|
|
124
|
+
cursor?: string | null;
|
|
125
|
+
} | Fetch, customFetch?: Fetch): Promise<SpaceSessionsResponse>;
|
|
118
126
|
byId(sessionId: string): SessionClient;
|
|
119
127
|
}
|
|
120
128
|
export type WebSocketConnectionState = {
|
|
121
|
-
state: "open" | "closed" | "error";
|
|
129
|
+
state: "connecting" | "reconnecting" | "open" | "closed" | "error";
|
|
122
130
|
willReconnect: boolean;
|
|
123
131
|
connectionId?: string | null;
|
|
132
|
+
attempt?: number;
|
|
133
|
+
delayMs?: number;
|
|
134
|
+
recoverable?: boolean;
|
|
124
135
|
};
|
|
125
136
|
export declare class SpaceEventsApi {
|
|
126
137
|
private readonly websocketClient;
|
|
@@ -157,6 +168,49 @@ export declare class SpaceUsageApi {
|
|
|
157
168
|
constructor(transport: HttpTransport, spaceId: string);
|
|
158
169
|
get(days?: number, customFetch?: Fetch): Promise<SpaceUsageResponse>;
|
|
159
170
|
}
|
|
171
|
+
export type SpaceChannelBindingRecord = {
|
|
172
|
+
id: string;
|
|
173
|
+
spaceId: string;
|
|
174
|
+
channelId: string;
|
|
175
|
+
config: Record<string, unknown> | null;
|
|
176
|
+
createdAt: string;
|
|
177
|
+
channel: {
|
|
178
|
+
id: string;
|
|
179
|
+
userUuid: string;
|
|
180
|
+
provider: string;
|
|
181
|
+
name: string;
|
|
182
|
+
status: string;
|
|
183
|
+
createdAt: string;
|
|
184
|
+
updatedAt: string;
|
|
185
|
+
} | null;
|
|
186
|
+
};
|
|
187
|
+
export declare class SpaceChannelsApi {
|
|
188
|
+
private readonly transport;
|
|
189
|
+
private readonly spaceId;
|
|
190
|
+
constructor(transport: HttpTransport, spaceId: string);
|
|
191
|
+
list(): Promise<SpaceChannelBindingRecord[]>;
|
|
192
|
+
bind(channelId: string, config?: Record<string, unknown> | null): Promise<SpaceChannelBindingRecord>;
|
|
193
|
+
unbind(channelId: string): Promise<{
|
|
194
|
+
ok: true;
|
|
195
|
+
}>;
|
|
196
|
+
}
|
|
197
|
+
export declare class SpaceEnvApi {
|
|
198
|
+
private readonly transport;
|
|
199
|
+
private readonly spaceId;
|
|
200
|
+
constructor(transport: HttpTransport, spaceId: string);
|
|
201
|
+
list(): Promise<{
|
|
202
|
+
env: SpaceEnvInput[];
|
|
203
|
+
}>;
|
|
204
|
+
create(input: SpaceEnvInput): Promise<{
|
|
205
|
+
env: SpaceEnvInput[];
|
|
206
|
+
}>;
|
|
207
|
+
update(name: string, value: string): Promise<{
|
|
208
|
+
env: SpaceEnvInput[];
|
|
209
|
+
}>;
|
|
210
|
+
remove(name: string): Promise<{
|
|
211
|
+
env: SpaceEnvInput[];
|
|
212
|
+
}>;
|
|
213
|
+
}
|
|
160
214
|
export declare class SpaceCheckpointsApi {
|
|
161
215
|
private readonly transport;
|
|
162
216
|
private readonly spaceId;
|
|
@@ -180,6 +234,9 @@ export declare class SpaceClient {
|
|
|
180
234
|
readonly access: SpaceAccessApi;
|
|
181
235
|
readonly checkpoints: SpaceCheckpointsApi;
|
|
182
236
|
readonly usage: SpaceUsageApi;
|
|
237
|
+
readonly channels: SpaceChannelsApi;
|
|
238
|
+
readonly env: SpaceEnvApi;
|
|
239
|
+
readonly invitations: SpaceInvitationsApi;
|
|
183
240
|
constructor(id: string, transport: HttpTransport, websocketClient: WebsocketClient | null);
|
|
184
241
|
get(customFetch?: Fetch): Promise<SpaceRecord>;
|
|
185
242
|
rename(name: string): Promise<{
|
package/dist/apis/spaces.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ensureRealtimeConnected } from "../realtime.js";
|
|
2
|
+
import { SpaceInvitationsApi } from "./invitations.js";
|
|
2
3
|
const DEFAULT_DEDUP_WINDOW_MS = 2000;
|
|
3
4
|
const toSessionEventName = (type) => {
|
|
4
5
|
switch (type) {
|
|
@@ -90,6 +91,19 @@ export class SpaceFilesApi {
|
|
|
90
91
|
body: JSON.stringify(input),
|
|
91
92
|
});
|
|
92
93
|
}
|
|
94
|
+
upload(files, dir = "") {
|
|
95
|
+
const params = new URLSearchParams();
|
|
96
|
+
if (dir)
|
|
97
|
+
params.set("dir", dir);
|
|
98
|
+
const query = params.toString();
|
|
99
|
+
const formData = new FormData();
|
|
100
|
+
for (const file of files)
|
|
101
|
+
formData.append("files", file);
|
|
102
|
+
return this.transport.request(`/api/spaces/${this.spaceId}/fs/upload${query ? `?${query}` : ""}`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
body: formData,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
93
107
|
}
|
|
94
108
|
class SessionMessagesClient {
|
|
95
109
|
transport;
|
|
@@ -106,6 +120,17 @@ class SessionMessagesClient {
|
|
|
106
120
|
fetch: customFetch,
|
|
107
121
|
});
|
|
108
122
|
}
|
|
123
|
+
get(messageId, optionsOrFetch, customFetch) {
|
|
124
|
+
const options = typeof optionsOrFetch === "function" ? undefined : optionsOrFetch;
|
|
125
|
+
const fetch = typeof optionsOrFetch === "function" ? optionsOrFetch : customFetch;
|
|
126
|
+
const params = new URLSearchParams();
|
|
127
|
+
if (options?.detail)
|
|
128
|
+
params.set("detail", options.detail);
|
|
129
|
+
const query = params.toString();
|
|
130
|
+
return this.transport.request(`/api/sessions/${this.sessionId}/messages/${messageId}${query ? `?${query}` : ""}`, {
|
|
131
|
+
fetch,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
109
134
|
listPaginated(options, customFetch) {
|
|
110
135
|
const params = new URLSearchParams();
|
|
111
136
|
if (options?.cursor !== undefined)
|
|
@@ -233,9 +258,17 @@ export class SpaceSessionsApi {
|
|
|
233
258
|
body: JSON.stringify(input ?? {}),
|
|
234
259
|
});
|
|
235
260
|
}
|
|
236
|
-
list(customFetch) {
|
|
237
|
-
|
|
238
|
-
|
|
261
|
+
list(optionsOrFetch, customFetch) {
|
|
262
|
+
const options = typeof optionsOrFetch === "function" ? undefined : optionsOrFetch;
|
|
263
|
+
const fetch = typeof optionsOrFetch === "function" ? optionsOrFetch : customFetch;
|
|
264
|
+
const params = new URLSearchParams();
|
|
265
|
+
if (options?.limit !== undefined)
|
|
266
|
+
params.set("limit", String(options.limit));
|
|
267
|
+
if (options?.cursor)
|
|
268
|
+
params.set("cursor", options.cursor);
|
|
269
|
+
const query = params.toString();
|
|
270
|
+
return this.transport.request(`/api/spaces/${this.spaceId}/sessions${query ? `?${query}` : ""}`, {
|
|
271
|
+
fetch,
|
|
239
272
|
});
|
|
240
273
|
}
|
|
241
274
|
byId(sessionId) {
|
|
@@ -326,6 +359,55 @@ export class SpaceUsageApi {
|
|
|
326
359
|
return this.transport.request(`/api/spaces/${this.spaceId}/usage?${params.toString()}`, { fetch: customFetch });
|
|
327
360
|
}
|
|
328
361
|
}
|
|
362
|
+
export class SpaceChannelsApi {
|
|
363
|
+
transport;
|
|
364
|
+
spaceId;
|
|
365
|
+
constructor(transport, spaceId) {
|
|
366
|
+
this.transport = transport;
|
|
367
|
+
this.spaceId = spaceId;
|
|
368
|
+
}
|
|
369
|
+
list() {
|
|
370
|
+
return this.transport.request(`/api/spaces/${this.spaceId}/channels`);
|
|
371
|
+
}
|
|
372
|
+
bind(channelId, config) {
|
|
373
|
+
return this.transport.request(`/api/spaces/${this.spaceId}/channels/${channelId}`, {
|
|
374
|
+
method: "POST",
|
|
375
|
+
headers: { "Content-Type": "application/json" },
|
|
376
|
+
body: JSON.stringify({ config: config ?? null }),
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
unbind(channelId) {
|
|
380
|
+
return this.transport.request(`/api/spaces/${this.spaceId}/channels/${channelId}`, { method: "DELETE" });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
export class SpaceEnvApi {
|
|
384
|
+
transport;
|
|
385
|
+
spaceId;
|
|
386
|
+
constructor(transport, spaceId) {
|
|
387
|
+
this.transport = transport;
|
|
388
|
+
this.spaceId = spaceId;
|
|
389
|
+
}
|
|
390
|
+
list() {
|
|
391
|
+
return this.transport.request(`/api/spaces/${this.spaceId}/env`);
|
|
392
|
+
}
|
|
393
|
+
create(input) {
|
|
394
|
+
return this.transport.request(`/api/spaces/${this.spaceId}/env`, {
|
|
395
|
+
method: "POST",
|
|
396
|
+
headers: { "Content-Type": "application/json" },
|
|
397
|
+
body: JSON.stringify(input),
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
update(name, value) {
|
|
401
|
+
return this.transport.request(`/api/spaces/${this.spaceId}/env/${encodeURIComponent(name)}`, {
|
|
402
|
+
method: "PUT",
|
|
403
|
+
headers: { "Content-Type": "application/json" },
|
|
404
|
+
body: JSON.stringify({ value }),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
remove(name) {
|
|
408
|
+
return this.transport.request(`/api/spaces/${this.spaceId}/env/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
409
|
+
}
|
|
410
|
+
}
|
|
329
411
|
export class SpaceCheckpointsApi {
|
|
330
412
|
transport;
|
|
331
413
|
spaceId;
|
|
@@ -357,6 +439,9 @@ export class SpaceClient {
|
|
|
357
439
|
access;
|
|
358
440
|
checkpoints;
|
|
359
441
|
usage;
|
|
442
|
+
channels;
|
|
443
|
+
env;
|
|
444
|
+
invitations;
|
|
360
445
|
constructor(id, transport, websocketClient) {
|
|
361
446
|
this.id = id;
|
|
362
447
|
this.transport = transport;
|
|
@@ -367,6 +452,9 @@ export class SpaceClient {
|
|
|
367
452
|
this.access = new SpaceAccessApi(transport, id);
|
|
368
453
|
this.checkpoints = new SpaceCheckpointsApi(transport, id);
|
|
369
454
|
this.usage = new SpaceUsageApi(transport, id);
|
|
455
|
+
this.channels = new SpaceChannelsApi(transport, id);
|
|
456
|
+
this.env = new SpaceEnvApi(transport, id);
|
|
457
|
+
this.invitations = new SpaceInvitationsApi(transport, id);
|
|
370
458
|
}
|
|
371
459
|
get(customFetch) {
|
|
372
460
|
return this.transport.request(`/api/spaces/${this.id}`, {
|
package/dist/client.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { SessionAccessApi } from "./apis/session-access.js";
|
|
|
6
6
|
import { SpaceClient, SpacesApi, type WebSocketConnectionState } from "./apis/spaces.js";
|
|
7
7
|
import { TasksApi } from "./apis/tasks.js";
|
|
8
8
|
import { UserApi } from "./apis/user.js";
|
|
9
|
+
import { PublicInviteApi } from "./apis/invitations.js";
|
|
9
10
|
import { type CohubClientOptions } from "./transport.js";
|
|
10
11
|
export declare class CohubClient {
|
|
11
12
|
readonly spaces: SpacesApi;
|
|
@@ -16,6 +17,7 @@ export declare class CohubClient {
|
|
|
16
17
|
readonly sessionAccess: SessionAccessApi;
|
|
17
18
|
readonly tasks: TasksApi;
|
|
18
19
|
readonly cronJobs: CronJobsApi;
|
|
20
|
+
readonly invite: PublicInviteApi;
|
|
19
21
|
private readonly transport;
|
|
20
22
|
private readonly websocketClient;
|
|
21
23
|
constructor(options?: CohubClientOptions);
|
package/dist/client.js
CHANGED
|
@@ -6,8 +6,10 @@ import { SessionAccessApi } from "./apis/session-access.js";
|
|
|
6
6
|
import { SpaceClient, SpacesApi } from "./apis/spaces.js";
|
|
7
7
|
import { TasksApi } from "./apis/tasks.js";
|
|
8
8
|
import { UserApi } from "./apis/user.js";
|
|
9
|
+
import { PublicInviteApi } from "./apis/invitations.js";
|
|
9
10
|
import { HttpTransport } from "./transport.js";
|
|
10
11
|
import { createWebsocketClient } from "./websocket.js";
|
|
12
|
+
import { resolveApiBaseUrl, resolveWebsocketUrl } from "./environment.js";
|
|
11
13
|
export class CohubClient {
|
|
12
14
|
spaces;
|
|
13
15
|
channels;
|
|
@@ -17,27 +19,49 @@ export class CohubClient {
|
|
|
17
19
|
sessionAccess;
|
|
18
20
|
tasks;
|
|
19
21
|
cronJobs;
|
|
22
|
+
invite;
|
|
20
23
|
transport;
|
|
21
24
|
websocketClient;
|
|
22
25
|
constructor(options = {}) {
|
|
26
|
+
const apiBaseUrl = resolveApiBaseUrl(options);
|
|
23
27
|
this.transport = new HttpTransport(options);
|
|
24
28
|
this.websocketClient = createWebsocketClient({
|
|
29
|
+
url: resolveWebsocketUrl({
|
|
30
|
+
env: options.websocket?.env ?? options.env,
|
|
31
|
+
url: options.websocket?.url,
|
|
32
|
+
}),
|
|
25
33
|
...options.websocket,
|
|
26
34
|
getAccessToken: options.getAccessToken,
|
|
27
35
|
});
|
|
28
36
|
this.spaces = new SpacesApi(this.transport);
|
|
29
37
|
this.channels = new ChannelsApi(this.transport);
|
|
30
|
-
this.user = new UserApi(this.transport,
|
|
31
|
-
this.models = new ModelsApi(
|
|
32
|
-
this.prompts = new PromptsApi(options.fetch ?? fetch,
|
|
38
|
+
this.user = new UserApi(this.transport, apiBaseUrl, options.setStoredAuthToken, options.clearStoredAuthToken);
|
|
39
|
+
this.models = new ModelsApi(this.transport);
|
|
40
|
+
this.prompts = new PromptsApi(options.fetch ?? fetch, apiBaseUrl);
|
|
33
41
|
this.sessionAccess = new SessionAccessApi(this.transport);
|
|
34
42
|
this.tasks = new TasksApi(this.transport);
|
|
35
43
|
this.cronJobs = new CronJobsApi(this.transport);
|
|
44
|
+
this.invite = new PublicInviteApi(this.transport);
|
|
36
45
|
}
|
|
37
46
|
space(spaceId) {
|
|
38
47
|
return new SpaceClient(spaceId, this.transport, this.websocketClient);
|
|
39
48
|
}
|
|
40
49
|
onConnection(handler) {
|
|
50
|
+
const connectingCleanup = this.websocketClient.on("connecting", (payload) => {
|
|
51
|
+
handler({
|
|
52
|
+
state: payload.isReconnect ? "reconnecting" : "connecting",
|
|
53
|
+
willReconnect: payload.isReconnect,
|
|
54
|
+
attempt: payload.attempt,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
const reconnectingCleanup = this.websocketClient.on("reconnecting", (payload) => {
|
|
58
|
+
handler({
|
|
59
|
+
state: "reconnecting",
|
|
60
|
+
willReconnect: true,
|
|
61
|
+
attempt: payload.attempt,
|
|
62
|
+
delayMs: payload.delayMs,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
41
65
|
const openCleanup = this.websocketClient.on("open", (payload) => {
|
|
42
66
|
handler({
|
|
43
67
|
state: "open",
|
|
@@ -51,10 +75,16 @@ export class CohubClient {
|
|
|
51
75
|
willReconnect: payload.willReconnect,
|
|
52
76
|
});
|
|
53
77
|
});
|
|
54
|
-
const errorCleanup = this.websocketClient.on("error", () => {
|
|
55
|
-
handler({
|
|
78
|
+
const errorCleanup = this.websocketClient.on("error", (payload) => {
|
|
79
|
+
handler({
|
|
80
|
+
state: "error",
|
|
81
|
+
willReconnect: payload.recoverable,
|
|
82
|
+
recoverable: payload.recoverable,
|
|
83
|
+
});
|
|
56
84
|
});
|
|
57
85
|
return () => {
|
|
86
|
+
connectingCleanup();
|
|
87
|
+
reconnectingCleanup();
|
|
58
88
|
openCleanup();
|
|
59
89
|
closeCleanup();
|
|
60
90
|
errorCleanup();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type CohubEnvironment = "prod" | "dev";
|
|
2
|
+
export declare const COHUB_ENVIRONMENTS: {
|
|
3
|
+
readonly prod: {
|
|
4
|
+
readonly apiBaseUrl: "https://api.cohub.run";
|
|
5
|
+
readonly websocketUrl: "wss://gateway.cohub.run/ws";
|
|
6
|
+
};
|
|
7
|
+
readonly dev: {
|
|
8
|
+
readonly apiBaseUrl: "https://api-dev.cohub.run";
|
|
9
|
+
readonly websocketUrl: "wss://gateway-dev.cohub.run/ws";
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export declare const resolveCohubEnvironment: (env?: CohubEnvironment) => CohubEnvironment;
|
|
13
|
+
export declare const normalizeBaseUrl: (url: string) => string;
|
|
14
|
+
export declare const normalizeWebsocketUrl: (input: string) => string;
|
|
15
|
+
export declare const resolveApiBaseUrl: (options?: {
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
env?: CohubEnvironment;
|
|
18
|
+
}) => string;
|
|
19
|
+
export declare const resolveWebsocketUrl: (options?: {
|
|
20
|
+
url?: string;
|
|
21
|
+
env?: CohubEnvironment;
|
|
22
|
+
}) => string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const COHUB_ENVIRONMENTS = {
|
|
2
|
+
prod: {
|
|
3
|
+
apiBaseUrl: "https://api.cohub.run",
|
|
4
|
+
websocketUrl: "wss://gateway.cohub.run/ws",
|
|
5
|
+
},
|
|
6
|
+
dev: {
|
|
7
|
+
apiBaseUrl: "https://api-dev.cohub.run",
|
|
8
|
+
websocketUrl: "wss://gateway-dev.cohub.run/ws",
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
const readRuntimeEnv = () => {
|
|
12
|
+
const runtime = globalThis;
|
|
13
|
+
return runtime.process?.env?.ENV;
|
|
14
|
+
};
|
|
15
|
+
export const resolveCohubEnvironment = (env) => {
|
|
16
|
+
if (env)
|
|
17
|
+
return env;
|
|
18
|
+
return readRuntimeEnv() === "dev" ? "dev" : "prod";
|
|
19
|
+
};
|
|
20
|
+
export const normalizeBaseUrl = (url) => url.trim().replace(/\/+$/, "");
|
|
21
|
+
export const normalizeWebsocketUrl = (input) => {
|
|
22
|
+
const trimmed = normalizeBaseUrl(input);
|
|
23
|
+
const withProtocol = trimmed
|
|
24
|
+
.replace(/^http:/, "ws:")
|
|
25
|
+
.replace(/^https:/, "wss:");
|
|
26
|
+
return withProtocol.endsWith("/ws") ? withProtocol : `${withProtocol}/ws`;
|
|
27
|
+
};
|
|
28
|
+
export const resolveApiBaseUrl = (options = {}) => {
|
|
29
|
+
if (options.baseUrl)
|
|
30
|
+
return normalizeBaseUrl(options.baseUrl);
|
|
31
|
+
return COHUB_ENVIRONMENTS[resolveCohubEnvironment(options.env)].apiBaseUrl;
|
|
32
|
+
};
|
|
33
|
+
export const resolveWebsocketUrl = (options = {}) => {
|
|
34
|
+
if (options.url)
|
|
35
|
+
return normalizeWebsocketUrl(options.url);
|
|
36
|
+
return COHUB_ENVIRONMENTS[resolveCohubEnvironment(options.env)].websocketUrl;
|
|
37
|
+
};
|
package/dist/http.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { SessionAccessApi } from "./apis/session-access.js";
|
|
|
6
6
|
import { SpaceClient, SpacesApi } from "./apis/spaces.js";
|
|
7
7
|
import { TasksApi } from "./apis/tasks.js";
|
|
8
8
|
import { UserApi } from "./apis/user.js";
|
|
9
|
+
import { PublicInviteApi } from "./apis/invitations.js";
|
|
9
10
|
import { HttpTransport, HttpError, type CohubClientOptions, type Fetch } from "./transport.js";
|
|
10
11
|
export declare class CohubHttpClient {
|
|
11
12
|
readonly spaces: SpacesApi;
|
|
@@ -16,6 +17,7 @@ export declare class CohubHttpClient {
|
|
|
16
17
|
readonly sessionAccess: SessionAccessApi;
|
|
17
18
|
readonly tasks: TasksApi;
|
|
18
19
|
readonly cronJobs: CronJobsApi;
|
|
20
|
+
readonly invite: PublicInviteApi;
|
|
19
21
|
private readonly transport;
|
|
20
22
|
constructor(options?: CohubClientOptions);
|
|
21
23
|
space(spaceId: string): SpaceClient;
|
package/dist/http.js
CHANGED
|
@@ -6,7 +6,9 @@ import { SessionAccessApi } from "./apis/session-access.js";
|
|
|
6
6
|
import { SpaceClient, SpacesApi } from "./apis/spaces.js";
|
|
7
7
|
import { TasksApi } from "./apis/tasks.js";
|
|
8
8
|
import { UserApi } from "./apis/user.js";
|
|
9
|
+
import { PublicInviteApi } from "./apis/invitations.js";
|
|
9
10
|
import { HttpTransport, HttpError } from "./transport.js";
|
|
11
|
+
import { resolveApiBaseUrl } from "./environment.js";
|
|
10
12
|
export class CohubHttpClient {
|
|
11
13
|
spaces;
|
|
12
14
|
channels;
|
|
@@ -16,17 +18,20 @@ export class CohubHttpClient {
|
|
|
16
18
|
sessionAccess;
|
|
17
19
|
tasks;
|
|
18
20
|
cronJobs;
|
|
21
|
+
invite;
|
|
19
22
|
transport;
|
|
20
23
|
constructor(options = {}) {
|
|
24
|
+
const apiBaseUrl = resolveApiBaseUrl(options);
|
|
21
25
|
this.transport = new HttpTransport(options);
|
|
22
26
|
this.spaces = new SpacesApi(this.transport);
|
|
23
27
|
this.channels = new ChannelsApi(this.transport);
|
|
24
|
-
this.user = new UserApi(this.transport,
|
|
25
|
-
this.models = new ModelsApi(
|
|
26
|
-
this.prompts = new PromptsApi(options.fetch ?? fetch,
|
|
28
|
+
this.user = new UserApi(this.transport, apiBaseUrl, options.setStoredAuthToken, options.clearStoredAuthToken);
|
|
29
|
+
this.models = new ModelsApi(this.transport);
|
|
30
|
+
this.prompts = new PromptsApi(options.fetch ?? fetch, apiBaseUrl);
|
|
27
31
|
this.sessionAccess = new SessionAccessApi(this.transport);
|
|
28
32
|
this.tasks = new TasksApi(this.transport);
|
|
29
33
|
this.cronJobs = new CronJobsApi(this.transport);
|
|
34
|
+
this.invite = new PublicInviteApi(this.transport);
|
|
30
35
|
}
|
|
31
36
|
space(spaceId) {
|
|
32
37
|
return new SpaceClient(spaceId, this.transport, null);
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { CohubHttpClient, createHttpClient } from "./http.js";
|
|
|
2
2
|
export { CohubClient, createCohubClient } from "./client.js";
|
|
3
3
|
export { WebsocketClient, createWebsocketClient } from "./websocket.js";
|
|
4
4
|
export { HttpError } from "./transport.js";
|
|
5
|
+
export { COHUB_ENVIRONMENTS, normalizeBaseUrl, normalizeWebsocketUrl, resolveApiBaseUrl, resolveCohubEnvironment, resolveWebsocketUrl, } from "./environment.js";
|
|
5
6
|
export type { CohubClientOptions, Fetch } from "./transport.js";
|
|
7
|
+
export type { CohubEnvironment } from "./environment.js";
|
|
6
8
|
export * from "./types.js";
|
|
7
|
-
export type { SessionEventName, SessionSubscriptionHandlers, SpaceEventName, WebSocketConnectionState } from "./apis/spaces.js";
|
|
9
|
+
export type { SessionEventName, SessionSubscriptionHandlers, SpaceChannelBindingRecord, SpaceEventName, WebSocketConnectionState } from "./apis/spaces.js";
|
package/dist/index.js
CHANGED
|
@@ -2,4 +2,5 @@ export { CohubHttpClient, createHttpClient } from "./http.js";
|
|
|
2
2
|
export { CohubClient, createCohubClient } from "./client.js";
|
|
3
3
|
export { WebsocketClient, createWebsocketClient } from "./websocket.js";
|
|
4
4
|
export { HttpError } from "./transport.js";
|
|
5
|
+
export { COHUB_ENVIRONMENTS, normalizeBaseUrl, normalizeWebsocketUrl, resolveApiBaseUrl, resolveCohubEnvironment, resolveWebsocketUrl, } from "./environment.js";
|
|
5
6
|
export * from "./types.js";
|
package/dist/realtime.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export function ensureRealtimeConnected(websocketClient) {
|
|
2
|
+
if (websocketClient.state === "open" || websocketClient.state === "connecting" || websocketClient.state === "reconnecting") {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
2
5
|
void websocketClient.connect().catch((error) => {
|
|
3
6
|
console.error("[CohubClient] Failed to connect realtime websocket:", error);
|
|
4
7
|
});
|
package/dist/transport.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { CohubEnvironment } from "./environment.js";
|
|
1
2
|
import type { WebsocketClientOptions } from "./websocket.js";
|
|
2
3
|
export type Fetch = typeof globalThis.fetch;
|
|
3
4
|
export type CohubClientOptions = {
|
|
5
|
+
env?: CohubEnvironment;
|
|
4
6
|
baseUrl?: string;
|
|
5
7
|
getAccessToken?: () => Promise<string | null> | string | null;
|
|
6
8
|
onUnauthorized?: () => Promise<void> | void;
|
package/dist/transport.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveApiBaseUrl } from "./environment.js";
|
|
1
2
|
export class HttpError extends Error {
|
|
2
3
|
status;
|
|
3
4
|
body;
|
|
@@ -14,7 +15,7 @@ export class HttpTransport {
|
|
|
14
15
|
getAccessToken;
|
|
15
16
|
onUnauthorized;
|
|
16
17
|
constructor(options = {}) {
|
|
17
|
-
this.baseUrl = options
|
|
18
|
+
this.baseUrl = resolveApiBaseUrl(options);
|
|
18
19
|
this.fetcher = options.fetch ?? fetch;
|
|
19
20
|
this.getAccessToken = options.getAccessToken;
|
|
20
21
|
this.onUnauthorized = options.onUnauthorized;
|
package/dist/types.d.ts
CHANGED
|
@@ -40,6 +40,22 @@ export type SpaceFsMoveInput = {
|
|
|
40
40
|
fromPath: string;
|
|
41
41
|
toPath: string;
|
|
42
42
|
};
|
|
43
|
+
export type SpaceFsUploadEntry = {
|
|
44
|
+
path: string;
|
|
45
|
+
name: string;
|
|
46
|
+
size: number;
|
|
47
|
+
mimeType: string | null;
|
|
48
|
+
mtimeMs: number;
|
|
49
|
+
};
|
|
50
|
+
export type SpaceFsUploadError = {
|
|
51
|
+
name: string;
|
|
52
|
+
code: "file_too_large" | "name_invalid" | "write_failed";
|
|
53
|
+
message: string;
|
|
54
|
+
};
|
|
55
|
+
export type SpaceFsUploadResponse = {
|
|
56
|
+
uploaded: SpaceFsUploadEntry[];
|
|
57
|
+
errors: SpaceFsUploadError[];
|
|
58
|
+
};
|
|
43
59
|
export type SessionBindingRecord = ProtocolSessionBindingRecord;
|
|
44
60
|
export type SessionRecord = ProtocolSessionRecord & {
|
|
45
61
|
bindings?: SessionBindingRecord[];
|
|
@@ -49,6 +65,10 @@ export type SessionRecord = ProtocolSessionRecord & {
|
|
|
49
65
|
totalOutputTokens?: number;
|
|
50
66
|
totalCost?: string | number | null;
|
|
51
67
|
};
|
|
68
|
+
export type SpaceGitInfo = {
|
|
69
|
+
giteaHost: string;
|
|
70
|
+
giteaUsername: string;
|
|
71
|
+
};
|
|
52
72
|
export type SpaceRecord = {
|
|
53
73
|
id: string;
|
|
54
74
|
userUuid: string;
|
|
@@ -69,6 +89,7 @@ export type SpaceRecord = {
|
|
|
69
89
|
status: string;
|
|
70
90
|
}[];
|
|
71
91
|
accessLevel?: "minimal";
|
|
92
|
+
gitInfo?: SpaceGitInfo | null;
|
|
72
93
|
};
|
|
73
94
|
export type SpaceBootstrapSource = {
|
|
74
95
|
type: "blank";
|
|
@@ -89,6 +110,10 @@ export type SessionMessagesResponse = {
|
|
|
89
110
|
session: SessionRecord;
|
|
90
111
|
messages: MessageRecord[];
|
|
91
112
|
};
|
|
113
|
+
export type SessionMessageResponse = {
|
|
114
|
+
session: SessionRecord;
|
|
115
|
+
message: MessageRecord;
|
|
116
|
+
};
|
|
92
117
|
export type SessionMessagesPaginatedResponse = {
|
|
93
118
|
session: SessionRecord;
|
|
94
119
|
messages: MessageRecord[];
|
|
@@ -134,6 +159,10 @@ export type SpaceChannelBindingInput = {
|
|
|
134
159
|
};
|
|
135
160
|
export type SpaceSessionsResponse = {
|
|
136
161
|
sessions: SessionRecord[];
|
|
162
|
+
pageInfo?: {
|
|
163
|
+
hasMore: boolean;
|
|
164
|
+
nextCursor: string | null;
|
|
165
|
+
};
|
|
137
166
|
};
|
|
138
167
|
export type UserSshKey = {
|
|
139
168
|
id: string;
|
|
@@ -245,3 +274,36 @@ export type SpaceUsageResponse = {
|
|
|
245
274
|
summary: SpaceUsageSummary;
|
|
246
275
|
days: number;
|
|
247
276
|
};
|
|
277
|
+
export type SpaceInvitation = {
|
|
278
|
+
token: string;
|
|
279
|
+
role: SpaceRole;
|
|
280
|
+
status: "active" | "revoked" | "exhausted";
|
|
281
|
+
useCount: number;
|
|
282
|
+
maxUses: number | null;
|
|
283
|
+
createdAt: string | null;
|
|
284
|
+
expiresInSeconds: number | null;
|
|
285
|
+
};
|
|
286
|
+
export type CreateInvitationInput = {
|
|
287
|
+
role?: SpaceRole;
|
|
288
|
+
ttlSeconds?: number;
|
|
289
|
+
maxUses?: number;
|
|
290
|
+
};
|
|
291
|
+
export type CreateInvitationResponse = {
|
|
292
|
+
token: string;
|
|
293
|
+
role: SpaceRole;
|
|
294
|
+
expiresAt: string;
|
|
295
|
+
maxUses: number | null;
|
|
296
|
+
};
|
|
297
|
+
export type InvitationDetail = {
|
|
298
|
+
token: string;
|
|
299
|
+
spaceId: string;
|
|
300
|
+
spaceName: string;
|
|
301
|
+
role: SpaceRole;
|
|
302
|
+
expiresInSeconds: number | null;
|
|
303
|
+
};
|
|
304
|
+
export type AcceptInvitationResponse = {
|
|
305
|
+
ok: true;
|
|
306
|
+
spaceId: string;
|
|
307
|
+
spaceName: string;
|
|
308
|
+
role: SpaceRole;
|
|
309
|
+
};
|
package/dist/websocket.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ChannelEnvelope } from "@neta-art/cohub-protocol/realtime";
|
|
2
2
|
import type { ContentBlock } from "@neta-art/cohub-protocol/core";
|
|
3
|
+
import type { CohubEnvironment } from "./environment.js";
|
|
3
4
|
export type WebsocketEventPayload = ChannelEnvelope;
|
|
4
5
|
export type WebSocketLike = {
|
|
5
6
|
readonly readyState: number;
|
|
@@ -12,6 +13,7 @@ export type WebSocketLike = {
|
|
|
12
13
|
};
|
|
13
14
|
export type WebSocketConstructor = new (url: string) => WebSocketLike;
|
|
14
15
|
export type WebsocketClientOptions = {
|
|
16
|
+
env?: CohubEnvironment;
|
|
15
17
|
url?: string;
|
|
16
18
|
autoReconnect?: boolean;
|
|
17
19
|
reconnectBaseDelayMs?: number;
|
|
@@ -22,8 +24,18 @@ export type WebsocketClientOptions = {
|
|
|
22
24
|
getAccessToken?: () => Promise<string | null> | string | null;
|
|
23
25
|
WebSocketImpl?: WebSocketConstructor;
|
|
24
26
|
};
|
|
25
|
-
export type WebsocketClientState = "idle" | "connecting" | "open" | "closed";
|
|
27
|
+
export type WebsocketClientState = "idle" | "connecting" | "reconnecting" | "open" | "closed";
|
|
26
28
|
export type WebsocketClientEvents = {
|
|
29
|
+
connecting: {
|
|
30
|
+
isReconnect: boolean;
|
|
31
|
+
attempt: number;
|
|
32
|
+
};
|
|
33
|
+
reconnecting: {
|
|
34
|
+
attempt: number;
|
|
35
|
+
delayMs: number;
|
|
36
|
+
reason?: string;
|
|
37
|
+
code?: number;
|
|
38
|
+
};
|
|
27
39
|
open: {
|
|
28
40
|
connectionId?: string | null;
|
|
29
41
|
};
|
|
@@ -34,6 +46,7 @@ export type WebsocketClientEvents = {
|
|
|
34
46
|
};
|
|
35
47
|
error: {
|
|
36
48
|
error: unknown;
|
|
49
|
+
recoverable: boolean;
|
|
37
50
|
};
|
|
38
51
|
event: WebsocketEventPayload;
|
|
39
52
|
ready: {
|
|
@@ -75,6 +88,7 @@ export declare class WebsocketClient {
|
|
|
75
88
|
private ws;
|
|
76
89
|
private pingTimer;
|
|
77
90
|
private reconnectTimer;
|
|
91
|
+
private reconnectTimerResolver;
|
|
78
92
|
private reconnectAttempt;
|
|
79
93
|
private manuallyClosed;
|
|
80
94
|
private connectPromise;
|
package/dist/websocket.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { realtimeEnvelopeSchema, } from "@neta-art/cohub-protocol/realtime";
|
|
2
|
+
import { resolveWebsocketUrl } from "./environment.js";
|
|
2
3
|
const createEventMap = () => ({
|
|
4
|
+
connecting: new Set(),
|
|
5
|
+
reconnecting: new Set(),
|
|
3
6
|
open: new Set(),
|
|
4
7
|
close: new Set(),
|
|
5
8
|
error: new Set(),
|
|
@@ -10,24 +13,9 @@ const createEventMap = () => ({
|
|
|
10
13
|
serverError: new Set(),
|
|
11
14
|
pong: new Set(),
|
|
12
15
|
});
|
|
13
|
-
const toWebSocketUrl = (input) => {
|
|
14
|
-
const base = (input?.trim() || "").replace(/\/$/, "");
|
|
15
|
-
if (base) {
|
|
16
|
-
if (base.startsWith("ws://") || base.startsWith("wss://"))
|
|
17
|
-
return `${base}/ws`;
|
|
18
|
-
if (base.startsWith("http://"))
|
|
19
|
-
return `${base.replace(/^http:/, "ws:")}/ws`;
|
|
20
|
-
if (base.startsWith("https://"))
|
|
21
|
-
return `${base.replace(/^https:/, "wss:")}/ws`;
|
|
22
|
-
}
|
|
23
|
-
if (typeof window !== "undefined") {
|
|
24
|
-
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
25
|
-
return `${protocol}//${window.location.host}/ws`;
|
|
26
|
-
}
|
|
27
|
-
return "ws://localhost:8788/ws";
|
|
28
|
-
};
|
|
16
|
+
const toWebSocketUrl = (input, env) => resolveWebsocketUrl({ url: input, env });
|
|
29
17
|
const normalizeOptions = (options = {}) => ({
|
|
30
|
-
url: toWebSocketUrl(options.url),
|
|
18
|
+
url: toWebSocketUrl(options.url, options.env),
|
|
31
19
|
autoReconnect: options.autoReconnect !== false,
|
|
32
20
|
reconnectBaseDelayMs: options.reconnectBaseDelayMs ?? 1000,
|
|
33
21
|
reconnectMaxDelayMs: options.reconnectMaxDelayMs ?? 15000,
|
|
@@ -35,6 +23,15 @@ const normalizeOptions = (options = {}) => ({
|
|
|
35
23
|
pongTimeoutMs: options.pongTimeoutMs ?? 15000,
|
|
36
24
|
debug: options.debug === true,
|
|
37
25
|
});
|
|
26
|
+
const formatCloseMessage = (code, reason) => `WebSocket closed: ${code ?? 0} ${reason || ""}`.trim();
|
|
27
|
+
const isRetryableCloseCode = (code) => {
|
|
28
|
+
if (code === 1000)
|
|
29
|
+
return false;
|
|
30
|
+
if (code === 4003)
|
|
31
|
+
return false;
|
|
32
|
+
return true;
|
|
33
|
+
};
|
|
34
|
+
const AUTH_CLOSE_REASON = "authentication failed";
|
|
38
35
|
class WebsocketAuthError extends Error {
|
|
39
36
|
constructor(message) {
|
|
40
37
|
super(message);
|
|
@@ -54,6 +51,7 @@ export class WebsocketClient {
|
|
|
54
51
|
ws = null;
|
|
55
52
|
pingTimer = null;
|
|
56
53
|
reconnectTimer = null;
|
|
54
|
+
reconnectTimerResolver = null;
|
|
57
55
|
reconnectAttempt = 0;
|
|
58
56
|
manuallyClosed = false;
|
|
59
57
|
connectPromise = null;
|
|
@@ -97,8 +95,11 @@ export class WebsocketClient {
|
|
|
97
95
|
return this.connectPromise;
|
|
98
96
|
if (this.state === "open" && this.ws?.readyState === WebSocket.OPEN)
|
|
99
97
|
return;
|
|
98
|
+
const isReconnect = this.reconnectAttempt > 0 || this.state === "reconnecting";
|
|
100
99
|
this.manuallyClosed = false;
|
|
101
|
-
this.
|
|
100
|
+
this.clearReconnectTimer();
|
|
101
|
+
this.state = isReconnect ? "reconnecting" : "connecting";
|
|
102
|
+
this.emit("connecting", { isReconnect, attempt: this.reconnectAttempt });
|
|
102
103
|
this.connectPromise = new Promise((resolve, reject) => {
|
|
103
104
|
const ws = new this.WebSocketImpl(this.url);
|
|
104
105
|
this.ws = ws;
|
|
@@ -119,7 +120,7 @@ export class WebsocketClient {
|
|
|
119
120
|
};
|
|
120
121
|
ws.onopen = async () => {
|
|
121
122
|
try {
|
|
122
|
-
this.log("connected", this.url);
|
|
123
|
+
this.log("connected", { url: this.url, isReconnect, attempt: this.reconnectAttempt });
|
|
123
124
|
this.startPingLoop();
|
|
124
125
|
await this.authenticate();
|
|
125
126
|
this.state = "open";
|
|
@@ -129,37 +130,36 @@ export class WebsocketClient {
|
|
|
129
130
|
}
|
|
130
131
|
catch (error) {
|
|
131
132
|
const authError = error instanceof Error ? error : new Error("authentication failed");
|
|
133
|
+
this.emit("error", { error: authError, recoverable: false });
|
|
132
134
|
rejectOnce(authError);
|
|
133
|
-
ws.close(4003,
|
|
135
|
+
ws.close(4003, AUTH_CLOSE_REASON);
|
|
134
136
|
}
|
|
135
137
|
};
|
|
136
138
|
ws.onmessage = (event) => {
|
|
137
139
|
this.handleMessage(event.data);
|
|
138
140
|
};
|
|
139
141
|
ws.onerror = (error) => {
|
|
140
|
-
this.emit("error", { error });
|
|
142
|
+
this.emit("error", { error, recoverable: !this.manuallyClosed });
|
|
141
143
|
};
|
|
142
144
|
ws.onclose = (event) => {
|
|
143
145
|
this.stopPingLoop();
|
|
144
|
-
const wasConnecting = this.state === "connecting";
|
|
146
|
+
const wasConnecting = this.state === "connecting" || this.state === "reconnecting";
|
|
145
147
|
this.state = "closed";
|
|
146
148
|
this.ws = null;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
+
const closeError = new Error(formatCloseMessage(event.code, event.reason));
|
|
150
|
+
this.rejectAuthWaiter(closeError);
|
|
151
|
+
const willReconnect = !this.manuallyClosed && this.autoReconnect && isRetryableCloseCode(event.code);
|
|
152
|
+
this.log("closed", { code: event.code, reason: event.reason, willReconnect, wasConnecting });
|
|
149
153
|
this.emit("close", {
|
|
150
154
|
code: event.code,
|
|
151
155
|
reason: event.reason,
|
|
152
156
|
willReconnect,
|
|
153
157
|
});
|
|
154
158
|
if (wasConnecting) {
|
|
155
|
-
rejectOnce(
|
|
156
|
-
if (event.code === 4001 && willReconnect) {
|
|
157
|
-
void this.scheduleReconnect();
|
|
158
|
-
}
|
|
159
|
-
return;
|
|
159
|
+
rejectOnce(closeError);
|
|
160
160
|
}
|
|
161
161
|
if (willReconnect) {
|
|
162
|
-
void this.scheduleReconnect();
|
|
162
|
+
void this.scheduleReconnect(event.code, event.reason);
|
|
163
163
|
}
|
|
164
164
|
};
|
|
165
165
|
});
|
|
@@ -253,12 +253,12 @@ export class WebsocketClient {
|
|
|
253
253
|
parsed = typeof raw === "string" ? JSON.parse(raw) : JSON.parse(String(raw));
|
|
254
254
|
}
|
|
255
255
|
catch {
|
|
256
|
-
this.emit("error", { error: new Error("invalid websocket payload") });
|
|
256
|
+
this.emit("error", { error: new Error("invalid websocket payload"), recoverable: true });
|
|
257
257
|
return;
|
|
258
258
|
}
|
|
259
259
|
const result = realtimeEnvelopeSchema.safeParse(parsed);
|
|
260
260
|
if (!result.success) {
|
|
261
|
-
this.emit("error", { error: new Error("invalid realtime envelope") });
|
|
261
|
+
this.emit("error", { error: new Error("invalid realtime envelope"), recoverable: true });
|
|
262
262
|
return;
|
|
263
263
|
}
|
|
264
264
|
const envelope = result.data;
|
|
@@ -359,7 +359,7 @@ export class WebsocketClient {
|
|
|
359
359
|
if (this.awaitingPong &&
|
|
360
360
|
this.pongDeadlineAt > 0 &&
|
|
361
361
|
Date.now() > this.pongDeadlineAt) {
|
|
362
|
-
this.emit("error", { error: new Error("websocket pong timeout") });
|
|
362
|
+
this.emit("error", { error: new Error("websocket pong timeout"), recoverable: true });
|
|
363
363
|
this.ws.close(4002, "pong timeout");
|
|
364
364
|
return;
|
|
365
365
|
}
|
|
@@ -376,22 +376,41 @@ export class WebsocketClient {
|
|
|
376
376
|
this.pongDeadlineAt = 0;
|
|
377
377
|
}
|
|
378
378
|
clearReconnectTimer() {
|
|
379
|
-
if (
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
379
|
+
if (this.reconnectTimer) {
|
|
380
|
+
clearTimeout(this.reconnectTimer);
|
|
381
|
+
this.reconnectTimer = null;
|
|
382
|
+
}
|
|
383
|
+
if (this.reconnectTimerResolver) {
|
|
384
|
+
const resolve = this.reconnectTimerResolver;
|
|
385
|
+
this.reconnectTimerResolver = null;
|
|
386
|
+
resolve();
|
|
387
|
+
}
|
|
383
388
|
}
|
|
384
|
-
async scheduleReconnect() {
|
|
389
|
+
async scheduleReconnect(code, reason) {
|
|
385
390
|
this.clearReconnectTimer();
|
|
391
|
+
const attempt = this.reconnectAttempt + 1;
|
|
386
392
|
const delay = Math.min(this.reconnectBaseDelayMs * 2 ** this.reconnectAttempt, this.reconnectMaxDelayMs);
|
|
387
|
-
this.reconnectAttempt
|
|
393
|
+
this.reconnectAttempt = attempt;
|
|
394
|
+
this.state = "reconnecting";
|
|
395
|
+
this.log("schedule reconnect", { attempt, delay, code, reason });
|
|
396
|
+
this.emit("reconnecting", {
|
|
397
|
+
attempt,
|
|
398
|
+
delayMs: delay,
|
|
399
|
+
code,
|
|
400
|
+
reason,
|
|
401
|
+
});
|
|
388
402
|
await new Promise((resolve) => {
|
|
389
|
-
this.
|
|
403
|
+
this.reconnectTimerResolver = resolve;
|
|
404
|
+
this.reconnectTimer = setTimeout(() => {
|
|
405
|
+
this.reconnectTimer = null;
|
|
406
|
+
this.reconnectTimerResolver = null;
|
|
407
|
+
resolve();
|
|
408
|
+
}, delay);
|
|
390
409
|
});
|
|
391
410
|
if (this.manuallyClosed)
|
|
392
411
|
return;
|
|
393
412
|
await this.connect().catch((error) => {
|
|
394
|
-
this.emit("error", { error });
|
|
413
|
+
this.emit("error", { error, recoverable: true });
|
|
395
414
|
});
|
|
396
415
|
}
|
|
397
416
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Cohub SDK for spaces, sessions, checkpoints, and realtime agent collaboration.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -43,14 +43,14 @@
|
|
|
43
43
|
"dist",
|
|
44
44
|
"README.md"
|
|
45
45
|
],
|
|
46
|
-
"scripts": {
|
|
47
|
-
"build": "tsc -p tsconfig.build.json",
|
|
48
|
-
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
49
|
-
},
|
|
50
46
|
"dependencies": {
|
|
51
|
-
"@neta-art/cohub-protocol": "
|
|
47
|
+
"@neta-art/cohub-protocol": "1.2.0"
|
|
52
48
|
},
|
|
53
49
|
"devDependencies": {
|
|
54
50
|
"typescript": "^6.0.3"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsc -p tsconfig.build.json",
|
|
54
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
55
55
|
}
|
|
56
|
-
}
|
|
56
|
+
}
|